toolcraft 0.0.93 → 0.0.94
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/README.md +74 -4
- package/composition.json +1 -1
- package/dist/agent-defs.d.ts +1 -0
- package/dist/agent-defs.js +1 -0
- package/dist/agent-human-in-loop.d.ts +1 -0
- package/dist/agent-human-in-loop.js +1 -0
- package/dist/agent-mcp-config.d.ts +1 -0
- package/dist/agent-mcp-config.js +1 -0
- package/dist/auth-store.d.ts +1 -0
- package/dist/auth-store.js +1 -0
- package/dist/cli.d.ts +4 -1
- package/dist/cli.js +23 -64
- package/dist/composition.json +1 -1
- package/dist/config-mutations.d.ts +1 -0
- package/dist/config-mutations.js +1 -0
- package/dist/frontmatter.d.ts +1 -0
- package/dist/frontmatter.js +1 -0
- package/dist/human-in-loop/gate.d.ts +5 -1
- package/dist/human-in-loop/gate.js +10 -8
- package/dist/human-in-loop/runner.js +1 -28
- package/dist/mcp.d.ts +4 -1
- package/dist/mcp.js +8 -51
- package/dist/process-runner.d.ts +1 -0
- package/dist/process-runner.js +1 -0
- package/dist/runtime/io.d.ts +5 -0
- package/dist/runtime/io.js +50 -0
- package/dist/sdk.d.ts +12 -8
- package/dist/sdk.js +7 -52
- package/dist/task-list.d.ts +1 -0
- package/dist/task-list.js +1 -0
- package/dist/testing/fakes.d.ts +20 -0
- package/dist/testing/fakes.js +83 -0
- package/dist/testing/fixtures.d.ts +8 -0
- package/dist/testing/fixtures.js +248 -0
- package/dist/testing/harness.d.ts +76 -0
- package/dist/testing/harness.js +391 -0
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.js +3 -0
- package/dist/testing/memory-fs.d.ts +11 -0
- package/dist/testing/memory-fs.js +61 -0
- package/dist/testing/parity.d.ts +25 -0
- package/dist/testing/parity.js +384 -0
- package/dist/testing/render-capture.d.ts +6 -0
- package/dist/testing/render-capture.js +54 -0
- package/dist/tiny-mcp-client.d.ts +1 -0
- package/dist/tiny-mcp-client.js +1 -0
- package/node_modules/@poe-code/agent-defs/package.json +2 -0
- package/node_modules/@poe-code/agent-human-in-loop/README.md +12 -2
- package/node_modules/@poe-code/agent-human-in-loop/package.json +2 -0
- package/node_modules/@poe-code/agent-mcp-config/README.md +7 -7
- package/node_modules/@poe-code/agent-mcp-config/package.json +2 -0
- package/node_modules/@poe-code/config-mutations/README.md +8 -8
- package/node_modules/@poe-code/config-mutations/package.json +2 -0
- package/node_modules/@poe-code/frontmatter/README.md +2 -2
- package/node_modules/@poe-code/frontmatter/package.json +2 -0
- package/node_modules/@poe-code/process-runner/README.md +1 -1
- package/node_modules/@poe-code/process-runner/package.json +2 -0
- package/node_modules/@poe-code/task-list/README.md +15 -3
- package/node_modules/@poe-code/task-list/package.json +2 -0
- package/node_modules/auth-store/README.md +15 -0
- package/node_modules/auth-store/package.json +2 -0
- package/node_modules/tiny-mcp-client/README.md +36 -36
- package/node_modules/tiny-mcp-client/package.json +2 -0
- package/node_modules/toolcraft-design/README.md +8 -4
- package/node_modules/toolcraft-design/dist/dashboard/terminal.js +48 -6
- package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/explorer/actions.js +1 -0
- package/node_modules/toolcraft-design/dist/explorer/events.d.ts +4 -0
- package/node_modules/toolcraft-design/dist/explorer/reducer.js +52 -4
- package/node_modules/toolcraft-design/dist/explorer/render/list.js +47 -20
- package/node_modules/toolcraft-design/dist/explorer/render/modal.js +13 -1
- package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +1 -1
- package/node_modules/toolcraft-design/dist/explorer/runtime.js +3 -0
- package/node_modules/toolcraft-design/dist/explorer/state.d.ts +9 -0
- package/node_modules/toolcraft-design/package.json +5 -0
- package/package.json +42 -2
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { stripAnsi } from "toolcraft-design";
|
|
2
|
+
import { ApprovalDeclinedError, ToolcraftBugError, UserError, assertCommandRequirements, resolveCommandSecrets } from "../index.js";
|
|
3
|
+
import { approvalStateMachine } from "../human-in-loop/state-machine.js";
|
|
4
|
+
import { invokeWithHumanInLoop } from "../human-in-loop/gate.js";
|
|
5
|
+
import { createRuntimeLogger } from "../runtime-logging.js";
|
|
6
|
+
import { createEnv, createFs, validateServices } from "../runtime/io.js";
|
|
7
|
+
import { filterSchemaForScope } from "../schema-scope.js";
|
|
8
|
+
import { validateObjectSchema } from "../sdk.js";
|
|
9
|
+
import { throwValidationErrors } from "../validation-errors.js";
|
|
10
|
+
import { fakeFetch } from "./fakes.js";
|
|
11
|
+
import { createMemoryFs } from "./memory-fs.js";
|
|
12
|
+
import { runParity } from "./parity.js";
|
|
13
|
+
import { createRenderCapture } from "./render-capture.js";
|
|
14
|
+
function isHandlerFs(value) {
|
|
15
|
+
return typeof value.readFile === "function";
|
|
16
|
+
}
|
|
17
|
+
function asMemoryFs(value) {
|
|
18
|
+
if (value === undefined || !isHandlerFs(value)) {
|
|
19
|
+
return createMemoryFs(value);
|
|
20
|
+
}
|
|
21
|
+
if ("snapshot" in value && "changes" in value) {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
const handlerFs = createFs(value);
|
|
25
|
+
const changes = [];
|
|
26
|
+
return {
|
|
27
|
+
readFile: (path, encoding) => handlerFs.readFile(path, encoding),
|
|
28
|
+
exists: (path) => handlerFs.exists(path),
|
|
29
|
+
lstat: (path) => handlerFs.lstat(path),
|
|
30
|
+
async writeFile(path, contents, options) {
|
|
31
|
+
await handlerFs.writeFile(path, contents, options);
|
|
32
|
+
changes.push({ op: "writeFile", path });
|
|
33
|
+
},
|
|
34
|
+
async rename(fromPath, toPath) {
|
|
35
|
+
await handlerFs.rename(fromPath, toPath);
|
|
36
|
+
changes.push({ op: "rename", path: fromPath, to: toPath });
|
|
37
|
+
},
|
|
38
|
+
async unlink(path) {
|
|
39
|
+
await handlerFs.unlink(path);
|
|
40
|
+
changes.push({ op: "unlink", path });
|
|
41
|
+
},
|
|
42
|
+
snapshot: () => ({}),
|
|
43
|
+
changes: () => changes.map((change) => ({ ...change }))
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function hasOwnMcpProxyConfig(group) {
|
|
47
|
+
const symbol = Object.getOwnPropertySymbols(group).find((candidate) => candidate.description === "toolcraft.group.config");
|
|
48
|
+
if (symbol === undefined) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
const config = group[symbol];
|
|
52
|
+
return config?.mcp !== undefined;
|
|
53
|
+
}
|
|
54
|
+
function findChild(group, segment) {
|
|
55
|
+
return group.children.find((child) => child.name === segment || child.aliases.includes(segment));
|
|
56
|
+
}
|
|
57
|
+
function resolveCommand(root, requestedPath) {
|
|
58
|
+
const path = requestedPath[0] === root.name ? requestedPath.slice(1) : requestedPath;
|
|
59
|
+
let group = root;
|
|
60
|
+
const canonicalPath = [];
|
|
61
|
+
for (let index = 0; index < path.length; index += 1) {
|
|
62
|
+
if (hasOwnMcpProxyConfig(group)) {
|
|
63
|
+
throw new UserError(`Commands under MCP proxy group "${group.name}" need a live server.`);
|
|
64
|
+
}
|
|
65
|
+
const segment = path[index];
|
|
66
|
+
const child = findChild(group, segment);
|
|
67
|
+
if (child === undefined) {
|
|
68
|
+
throw new UserError(`Unknown command path "${requestedPath.join(" ")}".`);
|
|
69
|
+
}
|
|
70
|
+
canonicalPath.push(child.name);
|
|
71
|
+
if (child.kind === "command") {
|
|
72
|
+
if (index !== path.length - 1) {
|
|
73
|
+
throw new UserError(`Command "${canonicalPath.join(" ")}" has no child commands.`);
|
|
74
|
+
}
|
|
75
|
+
return { command: child, path: canonicalPath };
|
|
76
|
+
}
|
|
77
|
+
group = child;
|
|
78
|
+
}
|
|
79
|
+
if (hasOwnMcpProxyConfig(group)) {
|
|
80
|
+
throw new UserError(`Commands under MCP proxy group "${group.name}" need a live server.`);
|
|
81
|
+
}
|
|
82
|
+
if (group.default !== undefined) {
|
|
83
|
+
return { command: group.default, path: [...canonicalPath, group.default.name] };
|
|
84
|
+
}
|
|
85
|
+
throw new UserError(`Command path "${requestedPath.join(" ")}" resolves to a group.`);
|
|
86
|
+
}
|
|
87
|
+
function createSealedEnv(declarations, env, secrets) {
|
|
88
|
+
const values = { ...(env ?? {}) };
|
|
89
|
+
for (const [name, value] of Object.entries(secrets ?? {})) {
|
|
90
|
+
const definition = declarations[name];
|
|
91
|
+
if (definition !== undefined) {
|
|
92
|
+
values[definition.env] = value;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return Object.freeze(values);
|
|
96
|
+
}
|
|
97
|
+
function isHumanInLoopPending(value) {
|
|
98
|
+
return (typeof value === "object" &&
|
|
99
|
+
value !== null &&
|
|
100
|
+
value.status === "pending-approval");
|
|
101
|
+
}
|
|
102
|
+
function createTaskList() {
|
|
103
|
+
const tasksById = new Map();
|
|
104
|
+
const tasks = {
|
|
105
|
+
name: "approvals",
|
|
106
|
+
stateMachine: approvalStateMachine,
|
|
107
|
+
async all() {
|
|
108
|
+
return [...tasksById.values()];
|
|
109
|
+
},
|
|
110
|
+
async get(id) {
|
|
111
|
+
const task = tasksById.get(id);
|
|
112
|
+
if (task === undefined) {
|
|
113
|
+
throw new Error(`Unknown task ${id}`);
|
|
114
|
+
}
|
|
115
|
+
return task;
|
|
116
|
+
},
|
|
117
|
+
async create(input) {
|
|
118
|
+
const id = input.id ?? String(tasksById.size + 1);
|
|
119
|
+
const task = {
|
|
120
|
+
list: "approvals",
|
|
121
|
+
id,
|
|
122
|
+
qualifiedId: `approvals:${id}`,
|
|
123
|
+
name: input.name,
|
|
124
|
+
state: approvalStateMachine.initial,
|
|
125
|
+
description: input.description ?? "",
|
|
126
|
+
metadata: input.metadata ?? {}
|
|
127
|
+
};
|
|
128
|
+
tasksById.set(id, task);
|
|
129
|
+
return task;
|
|
130
|
+
},
|
|
131
|
+
async update(id, patch) {
|
|
132
|
+
const task = await tasks.get(id);
|
|
133
|
+
Object.assign(task, patch);
|
|
134
|
+
return task;
|
|
135
|
+
},
|
|
136
|
+
async fire(id) {
|
|
137
|
+
return tasks.get(id);
|
|
138
|
+
},
|
|
139
|
+
async canFire() {
|
|
140
|
+
return false;
|
|
141
|
+
},
|
|
142
|
+
async events() {
|
|
143
|
+
return [];
|
|
144
|
+
},
|
|
145
|
+
async delete(id) {
|
|
146
|
+
tasksById.delete(id);
|
|
147
|
+
},
|
|
148
|
+
async move(id) {
|
|
149
|
+
return tasks.get(id);
|
|
150
|
+
},
|
|
151
|
+
async reorder(ids) {
|
|
152
|
+
return Promise.all(ids.map((id) => tasks.get(id)));
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
return {
|
|
156
|
+
list: () => tasks,
|
|
157
|
+
async lists() {
|
|
158
|
+
return ["approvals"];
|
|
159
|
+
},
|
|
160
|
+
async allTasks() {
|
|
161
|
+
return tasks.all();
|
|
162
|
+
},
|
|
163
|
+
async get(qualifiedId) {
|
|
164
|
+
return tasks.get(qualifiedId.split(":").at(-1) ?? qualifiedId);
|
|
165
|
+
},
|
|
166
|
+
async moveBetweenLists(qualifiedId) {
|
|
167
|
+
return this.get(qualifiedId);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
export function createCommandTestHarness(root, options = {}) {
|
|
172
|
+
const services = options.services ?? {};
|
|
173
|
+
validateServices(services);
|
|
174
|
+
const memoryFs = asMemoryFs(options.fs);
|
|
175
|
+
const runtimeFetch = Array.isArray(options.fetch)
|
|
176
|
+
? fakeFetch(options.fetch)
|
|
177
|
+
: (options.fetch ?? fakeFetch([]));
|
|
178
|
+
const cumulativeTimeline = [];
|
|
179
|
+
let sequence = 0;
|
|
180
|
+
const append = (runTimeline, event) => {
|
|
181
|
+
const sequenced = { seq: ++sequence, ...event };
|
|
182
|
+
runTimeline.push(sequenced);
|
|
183
|
+
cumulativeTimeline.push(sequenced);
|
|
184
|
+
};
|
|
185
|
+
return {
|
|
186
|
+
fs: memoryFs,
|
|
187
|
+
timeline: cumulativeTimeline,
|
|
188
|
+
async parity(requestedPath, inputParams = {}) {
|
|
189
|
+
const resolved = resolveCommand(root, requestedPath);
|
|
190
|
+
const sealedEnv = createSealedEnv(resolved.command.secrets, options.env, options.secrets);
|
|
191
|
+
return runParity(root, resolved, inputParams, {
|
|
192
|
+
services,
|
|
193
|
+
env: Object.fromEntries(Object.entries(sealedEnv).flatMap(([key, value]) => value === undefined ? [] : [[key, value]])),
|
|
194
|
+
fs: memoryFs,
|
|
195
|
+
fetch: runtimeFetch,
|
|
196
|
+
apiVersion: options.apiVersion
|
|
197
|
+
});
|
|
198
|
+
},
|
|
199
|
+
async run(requestedPath, inputParams = {}) {
|
|
200
|
+
const logs = [];
|
|
201
|
+
const progress = [];
|
|
202
|
+
const confirmations = [];
|
|
203
|
+
const timeline = [];
|
|
204
|
+
const rendered = {};
|
|
205
|
+
const initialChangeCount = memoryFs.changes().length;
|
|
206
|
+
let stage = "resolve";
|
|
207
|
+
let value;
|
|
208
|
+
const result = (overrides = {}) => ({
|
|
209
|
+
ok: false,
|
|
210
|
+
value,
|
|
211
|
+
error: undefined,
|
|
212
|
+
failedAt: stage,
|
|
213
|
+
pending: false,
|
|
214
|
+
logs,
|
|
215
|
+
progress,
|
|
216
|
+
confirmations,
|
|
217
|
+
timeline,
|
|
218
|
+
fsChanges: memoryFs.changes().slice(initialChangeCount),
|
|
219
|
+
rendered,
|
|
220
|
+
...overrides
|
|
221
|
+
});
|
|
222
|
+
try {
|
|
223
|
+
const resolved = resolveCommand(root, requestedPath);
|
|
224
|
+
const command = resolved.command;
|
|
225
|
+
const commandPath = resolved.path.join(".");
|
|
226
|
+
stage = "secrets";
|
|
227
|
+
const sealedEnv = createSealedEnv(command.secrets, options.env, options.secrets);
|
|
228
|
+
const commandSecrets = resolveCommandSecrets(command, sealedEnv);
|
|
229
|
+
const diagnostics = createRuntimeLogger({
|
|
230
|
+
level: options.logLevel ?? "debug",
|
|
231
|
+
logger: (event) => logs.push(event)
|
|
232
|
+
});
|
|
233
|
+
const instrumentedFs = {
|
|
234
|
+
readFile: (path, encoding) => memoryFs.readFile(path, encoding),
|
|
235
|
+
exists: (path) => memoryFs.exists(path),
|
|
236
|
+
lstat: (path) => memoryFs.lstat(path),
|
|
237
|
+
async writeFile(path, contents, fsOptions) {
|
|
238
|
+
await memoryFs.writeFile(path, contents, fsOptions);
|
|
239
|
+
append(timeline, { kind: "fs", op: "writeFile", path });
|
|
240
|
+
},
|
|
241
|
+
async rename(fromPath, toPath) {
|
|
242
|
+
await memoryFs.rename(fromPath, toPath);
|
|
243
|
+
append(timeline, { kind: "fs", op: "rename", path: fromPath });
|
|
244
|
+
},
|
|
245
|
+
async unlink(path) {
|
|
246
|
+
await memoryFs.unlink(path);
|
|
247
|
+
append(timeline, { kind: "fs", op: "unlink", path });
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const instrumentedFetch = async (input, init) => {
|
|
251
|
+
const request = new Request(input, init);
|
|
252
|
+
append(timeline, {
|
|
253
|
+
kind: "fetch",
|
|
254
|
+
method: request.method,
|
|
255
|
+
url: request.url
|
|
256
|
+
});
|
|
257
|
+
return runtimeFetch(request);
|
|
258
|
+
};
|
|
259
|
+
const instrumentedServices = Object.fromEntries(Object.entries(services).map(([serviceName, service]) => {
|
|
260
|
+
if ((typeof service !== "object" || service === null) &&
|
|
261
|
+
typeof service !== "function") {
|
|
262
|
+
return [serviceName, service];
|
|
263
|
+
}
|
|
264
|
+
return [
|
|
265
|
+
serviceName,
|
|
266
|
+
new Proxy(service, {
|
|
267
|
+
get(target, property, receiver) {
|
|
268
|
+
const member = Reflect.get(target, property, receiver);
|
|
269
|
+
if (typeof member !== "function") {
|
|
270
|
+
return member;
|
|
271
|
+
}
|
|
272
|
+
return (...args) => {
|
|
273
|
+
append(timeline, {
|
|
274
|
+
kind: "service",
|
|
275
|
+
service: serviceName,
|
|
276
|
+
method: String(property),
|
|
277
|
+
args
|
|
278
|
+
});
|
|
279
|
+
return Reflect.apply(member, receiver, args);
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
})
|
|
283
|
+
];
|
|
284
|
+
}));
|
|
285
|
+
const baseContext = {
|
|
286
|
+
...instrumentedServices,
|
|
287
|
+
secrets: commandSecrets,
|
|
288
|
+
fetch: instrumentedFetch,
|
|
289
|
+
fs: instrumentedFs,
|
|
290
|
+
env: createEnv(new Proxy(sealedEnv, {
|
|
291
|
+
get(target, property, receiver) {
|
|
292
|
+
if (typeof property === "string") {
|
|
293
|
+
append(timeline, { kind: "env", key: property });
|
|
294
|
+
}
|
|
295
|
+
return Reflect.get(target, property, receiver);
|
|
296
|
+
}
|
|
297
|
+
})),
|
|
298
|
+
diagnostics,
|
|
299
|
+
progress(message) {
|
|
300
|
+
progress.push(message);
|
|
301
|
+
append(timeline, { kind: "progress", message });
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
stage = "requirements";
|
|
305
|
+
await assertCommandRequirements(command, { ...baseContext, params: undefined }, { apiVersion: options.apiVersion, env: sealedEnv });
|
|
306
|
+
stage = "params";
|
|
307
|
+
const schema = filterSchemaForScope(command.params, "sdk");
|
|
308
|
+
if (schema === undefined || schema.kind !== "object") {
|
|
309
|
+
throw new ToolcraftBugError(`command "${command.name}" must define an object params schema for SDK.`);
|
|
310
|
+
}
|
|
311
|
+
const errors = [];
|
|
312
|
+
const params = validateObjectSchema(schema, inputParams, "", errors);
|
|
313
|
+
throwValidationErrors(errors);
|
|
314
|
+
const decide = async (request) => {
|
|
315
|
+
confirmations.push(request);
|
|
316
|
+
const approved = options.confirmations === "decline"
|
|
317
|
+
? false
|
|
318
|
+
: typeof options.confirmations === "function"
|
|
319
|
+
? await options.confirmations(request)
|
|
320
|
+
: true;
|
|
321
|
+
append(timeline, { kind: "confirm", message: request.message, approved });
|
|
322
|
+
return approved;
|
|
323
|
+
};
|
|
324
|
+
stage = "confirm";
|
|
325
|
+
if (command.confirm && !command.humanInLoop) {
|
|
326
|
+
if (!(await decide({ message: "Proceed?" }))) {
|
|
327
|
+
throw new ApprovalDeclinedError({ commandPath });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const provider = {
|
|
331
|
+
id: "toolcraft-test-harness",
|
|
332
|
+
async requestApproval(request) {
|
|
333
|
+
return (await decide({
|
|
334
|
+
message: request.message,
|
|
335
|
+
declineInputPrompt: request.declineInputPrompt
|
|
336
|
+
}))
|
|
337
|
+
? { outcome: "approved" }
|
|
338
|
+
: { outcome: "declined" };
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const commandWithTrackedHandler = {
|
|
342
|
+
...command,
|
|
343
|
+
async handler(context) {
|
|
344
|
+
stage = "handler";
|
|
345
|
+
return command.handler(context);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
const invoked = await invokeWithHumanInLoop(commandWithTrackedHandler, { ...baseContext, params }, { provider, taskList: createTaskList() }, commandPath, {
|
|
349
|
+
spawnRunner: false,
|
|
350
|
+
async enqueueApproval({ tasks, payload }) {
|
|
351
|
+
const approvalId = `test-harness:${payload.commandPath}`;
|
|
352
|
+
const pending = {
|
|
353
|
+
status: "pending-approval",
|
|
354
|
+
approvalId,
|
|
355
|
+
message: payload.message,
|
|
356
|
+
enqueuedAt: ""
|
|
357
|
+
};
|
|
358
|
+
await tasks.create({
|
|
359
|
+
id: approvalId,
|
|
360
|
+
name: payload.commandPath,
|
|
361
|
+
metadata: { ...payload, approvalId, enqueuedAt: "" }
|
|
362
|
+
});
|
|
363
|
+
return { approvalId, pending };
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
value = invoked;
|
|
367
|
+
if (isHumanInLoopPending(invoked)) {
|
|
368
|
+
return result({ ok: true, failedAt: undefined, pending: true });
|
|
369
|
+
}
|
|
370
|
+
stage = "render";
|
|
371
|
+
if (command.render?.rich) {
|
|
372
|
+
const capture = createRenderCapture();
|
|
373
|
+
command.render.rich(invoked, capture.primitives);
|
|
374
|
+
rendered.rich = capture.output();
|
|
375
|
+
}
|
|
376
|
+
if (command.render?.markdown) {
|
|
377
|
+
const capture = createRenderCapture();
|
|
378
|
+
rendered.markdown = stripAnsi(command.render.markdown(invoked, capture.primitives));
|
|
379
|
+
}
|
|
380
|
+
if (command.render?.json) {
|
|
381
|
+
const capture = createRenderCapture();
|
|
382
|
+
rendered.json = command.render.json(invoked, capture.primitives);
|
|
383
|
+
}
|
|
384
|
+
return result({ ok: true, failedAt: undefined });
|
|
385
|
+
}
|
|
386
|
+
catch (error) {
|
|
387
|
+
return result({ error });
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { createCommandTestHarness, type CommandTestHarness, type ConfirmationRequest, type EffectEvent, type HarnessOptions, type PipelineStage, type RunResult } from "./harness.js";
|
|
2
|
+
export { fakeFetch, fakeService, type FetchRoute, type ServiceCall } from "./fakes.js";
|
|
3
|
+
export { createMemoryFs, type FsChange, type MemoryFs } from "./memory-fs.js";
|
|
4
|
+
export type { ParityResult, SurfaceOutcome } from "./parity.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { HandlerFs } from "../index.js";
|
|
2
|
+
export type FsChange = {
|
|
3
|
+
op: "writeFile" | "rename" | "unlink";
|
|
4
|
+
path: string;
|
|
5
|
+
to?: string;
|
|
6
|
+
};
|
|
7
|
+
export interface MemoryFs extends HandlerFs {
|
|
8
|
+
snapshot(): Record<string, string>;
|
|
9
|
+
changes(): FsChange[];
|
|
10
|
+
}
|
|
11
|
+
export declare function createMemoryFs(files?: Record<string, string>): MemoryFs;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
function missingFileError(syscall, path, dest) {
|
|
2
|
+
const target = dest === undefined ? `'${path}'` : `'${path}' -> '${dest}'`;
|
|
3
|
+
const error = new Error(`ENOENT: no such file or directory, ${syscall} ${target}`);
|
|
4
|
+
error.code = "ENOENT";
|
|
5
|
+
error.errno = -2;
|
|
6
|
+
error.path = path;
|
|
7
|
+
error.syscall = syscall;
|
|
8
|
+
if (dest !== undefined) {
|
|
9
|
+
error.dest = dest;
|
|
10
|
+
}
|
|
11
|
+
return error;
|
|
12
|
+
}
|
|
13
|
+
export function createMemoryFs(files = {}) {
|
|
14
|
+
const contents = new Map(Object.entries(files).map(([path, value]) => [path, Buffer.from(value, "utf8")]));
|
|
15
|
+
const changeLog = [];
|
|
16
|
+
return {
|
|
17
|
+
async readFile(path, encoding = "utf8") {
|
|
18
|
+
const value = contents.get(path);
|
|
19
|
+
if (value === undefined) {
|
|
20
|
+
throw missingFileError("open", path);
|
|
21
|
+
}
|
|
22
|
+
return value.toString(encoding);
|
|
23
|
+
},
|
|
24
|
+
async writeFile(path, value, options) {
|
|
25
|
+
contents.set(path, Buffer.from(value, options?.encoding ?? "utf8"));
|
|
26
|
+
changeLog.push({ op: "writeFile", path });
|
|
27
|
+
},
|
|
28
|
+
async exists(path) {
|
|
29
|
+
return contents.has(path);
|
|
30
|
+
},
|
|
31
|
+
async lstat(path) {
|
|
32
|
+
if (!contents.has(path)) {
|
|
33
|
+
throw missingFileError("lstat", path);
|
|
34
|
+
}
|
|
35
|
+
return { isSymbolicLink: () => false };
|
|
36
|
+
},
|
|
37
|
+
async rename(fromPath, toPath) {
|
|
38
|
+
const value = contents.get(fromPath);
|
|
39
|
+
if (value === undefined) {
|
|
40
|
+
throw missingFileError("rename", fromPath, toPath);
|
|
41
|
+
}
|
|
42
|
+
if (fromPath !== toPath) {
|
|
43
|
+
contents.set(toPath, value);
|
|
44
|
+
contents.delete(fromPath);
|
|
45
|
+
}
|
|
46
|
+
changeLog.push({ op: "rename", path: fromPath, to: toPath });
|
|
47
|
+
},
|
|
48
|
+
async unlink(path) {
|
|
49
|
+
if (!contents.delete(path)) {
|
|
50
|
+
throw missingFileError("unlink", path);
|
|
51
|
+
}
|
|
52
|
+
changeLog.push({ op: "unlink", path });
|
|
53
|
+
},
|
|
54
|
+
snapshot() {
|
|
55
|
+
return Object.fromEntries(Array.from(contents, ([path, value]) => [path, value.toString("utf8")]));
|
|
56
|
+
},
|
|
57
|
+
changes() {
|
|
58
|
+
return changeLog.map((change) => ({ ...change }));
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { Command, Group, HandlerFs } from "../index.js";
|
|
2
|
+
export interface SurfaceOutcome {
|
|
3
|
+
ok: boolean;
|
|
4
|
+
value: unknown;
|
|
5
|
+
error: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface ParityResult {
|
|
8
|
+
sdk: SurfaceOutcome;
|
|
9
|
+
mcp: SurfaceOutcome;
|
|
10
|
+
cli: SurfaceOutcome;
|
|
11
|
+
agree: boolean;
|
|
12
|
+
diff?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ParityOptions<TServices extends object> {
|
|
15
|
+
services: TServices;
|
|
16
|
+
env?: Record<string, string>;
|
|
17
|
+
fs: HandlerFs;
|
|
18
|
+
fetch: typeof globalThis.fetch;
|
|
19
|
+
apiVersion?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface ParityCommand {
|
|
22
|
+
command: Command<any, any, any, any>;
|
|
23
|
+
path: string[];
|
|
24
|
+
}
|
|
25
|
+
export declare function runParity<TServices extends object>(root: Group<any>, resolved: ParityCommand, params: Record<string, unknown>, options: ParityOptions<TServices>): Promise<ParityResult>;
|