toolcraft 0.0.93 → 0.0.95
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 +24 -65
- 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,50 @@
|
|
|
1
|
+
import { access, lstat, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
export const RESERVED_SERVICE_NAMES = new Set([
|
|
3
|
+
"params",
|
|
4
|
+
"secrets",
|
|
5
|
+
"fetch",
|
|
6
|
+
"fs",
|
|
7
|
+
"env",
|
|
8
|
+
"diagnostics",
|
|
9
|
+
"progress",
|
|
10
|
+
"runtimeOptions",
|
|
11
|
+
"root"
|
|
12
|
+
]);
|
|
13
|
+
const RESERVED_SERVICE_NAMES_MESSAGE = "Available reserved names: params, secrets, fetch, fs, env, diagnostics, progress, runtimeOptions, root.";
|
|
14
|
+
export function createFs(fs) {
|
|
15
|
+
if (fs !== undefined) {
|
|
16
|
+
return fs;
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
readFile: async (path, encoding = "utf8") => readFile(path, { encoding }),
|
|
20
|
+
writeFile: async (path, contents, options) => {
|
|
21
|
+
await writeFile(path, contents, options);
|
|
22
|
+
},
|
|
23
|
+
exists: async (path) => {
|
|
24
|
+
try {
|
|
25
|
+
await access(path);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
lstat: async (path) => lstat(path),
|
|
33
|
+
rename: async (fromPath, toPath) => rename(fromPath, toPath),
|
|
34
|
+
unlink: async (path) => unlink(path)
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
export function createEnv(values = process.env) {
|
|
38
|
+
return {
|
|
39
|
+
get(key) {
|
|
40
|
+
return values[key];
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function validateServices(services) {
|
|
45
|
+
for (const name of Object.keys(services)) {
|
|
46
|
+
if (RESERVED_SERVICE_NAMES.has(name)) {
|
|
47
|
+
throw new Error(`Service name "${name}" is reserved. Choose a different name. ${RESERVED_SERVICE_NAMES_MESSAGE}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/sdk.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { ObjectSchema, Static } from "toolcraft-schema";
|
|
2
|
-
import type { Group, LogLevel, RuntimeLoggerInput, Scope } from "./index.js";
|
|
2
|
+
import type { Group, HandlerFs, LogLevel, RuntimeLoggerInput, Scope } from "./index.js";
|
|
3
3
|
import { type ErrorReportsOption } from "./error-report.js";
|
|
4
4
|
import type { HumanInLoopPending, HumanInLoopRuntimeOptions } from "./human-in-loop/index.js";
|
|
5
|
+
import { type ValidationError } from "./validation-errors.js";
|
|
5
6
|
type ScopeInput = readonly Scope[] | undefined;
|
|
6
7
|
type HumanInLoopMode = "sync" | "async";
|
|
7
8
|
type HumanInLoopModeInput = HumanInLoopMode | null | undefined;
|
|
@@ -30,7 +31,7 @@ type CamelCase<TValue extends string> = JoinCamelWords<SplitCamelWords<TValue>>;
|
|
|
30
31
|
type Camelize<TValue> = TValue extends Primitive ? TValue : TValue extends readonly (infer TItem)[] ? Array<Camelize<TItem>> : TValue extends object ? {
|
|
31
32
|
[TKey in keyof TValue as TKey extends string ? CamelCase<TKey> : TKey]: Camelize<TValue[TKey]>;
|
|
32
33
|
} : TValue;
|
|
33
|
-
type SDKResult<TResult, THumanInLoopMode extends HumanInLoopMode | undefined> = THumanInLoopMode extends "async" ? HumanInLoopPending : TResult;
|
|
34
|
+
type SDKResult<TResult, THumanInLoopMode extends HumanInLoopMode | undefined> = [THumanInLoopMode] extends ["async"] ? ["async"] extends [THumanInLoopMode] ? HumanInLoopPending : TResult : TResult;
|
|
34
35
|
type SDKMethod<TParamsSchema extends ObjectSchema<any>, TResult> = (params: Camelize<Static<TParamsSchema>>) => Promise<TResult>;
|
|
35
36
|
type UnionToIntersection<TValue> = (TValue extends unknown ? (value: TValue) => void : never) extends (value: infer TResult) => void ? TResult : never;
|
|
36
37
|
type Simplify<TValue> = {
|
|
@@ -62,7 +63,9 @@ type SDKNodeShape<TNode, TInheritedScope extends ScopeInput, TInheritedHumanInLo
|
|
|
62
63
|
type SDKChildrenShape<TChildren, TInheritedScope extends ScopeInput, TInheritedHumanInLoopMode extends HumanInLoopMode | undefined> = Simplify<UnionToIntersection<SDKNodeShape<RawChildrenValue<TChildren>, TInheritedScope, TInheritedHumanInLoopMode>>>;
|
|
63
64
|
export interface CreateSDKOptions<TServices extends object = Record<string, unknown>> {
|
|
64
65
|
approvals?: boolean;
|
|
66
|
+
env?: Record<string, string>;
|
|
65
67
|
fetch?: typeof globalThis.fetch;
|
|
68
|
+
fs?: HandlerFs;
|
|
66
69
|
services?: TServices;
|
|
67
70
|
casing?: "camel";
|
|
68
71
|
humanInLoop?: HumanInLoopRuntimeOptions;
|
|
@@ -72,10 +75,11 @@ export interface CreateSDKOptions<TServices extends object = Record<string, unkn
|
|
|
72
75
|
logLevel?: LogLevel;
|
|
73
76
|
logger?: RuntimeLoggerInput;
|
|
74
77
|
}
|
|
75
|
-
export declare function
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
80
|
-
|
|
78
|
+
export declare function validateObjectSchema(schema: ObjectSchema<any>, value: unknown, label: string, errors: ValidationError[]): Record<string, unknown>;
|
|
79
|
+
type SDKRootShape<TRoot> = TRoot extends {
|
|
80
|
+
readonly __agentKitGroupTypeInfo: {
|
|
81
|
+
children: infer TChildren extends readonly unknown[];
|
|
82
|
+
};
|
|
83
|
+
} ? SDKChildrenShape<TChildren, undefined, undefined> : Record<string, unknown>;
|
|
84
|
+
export declare function createSDK<TRoot extends Group<any>, TServices extends object = Record<string, unknown>>(root: TRoot, options?: CreateSDKOptions<TServices>): SDKRootShape<TRoot>;
|
|
81
85
|
export {};
|
package/dist/sdk.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { access, lstat, readFile, rename, unlink, writeFile } from "node:fs/promises";
|
|
2
1
|
import { ToolcraftBugError, UserError, assertCommandRequirements, resolveCommandSecrets } from "./index.js";
|
|
3
2
|
import { writeErrorReport } from "./error-report.js";
|
|
4
3
|
import { mergeApprovalsGroup } from "./human-in-loop/approvals-commands.js";
|
|
@@ -10,18 +9,7 @@ import { enableSourceMaps } from "./stack-trim.js";
|
|
|
10
9
|
import { suggest } from "./suggest.js";
|
|
11
10
|
import { throwValidationErrors } from "./validation-errors.js";
|
|
12
11
|
import { createRuntimeLogger } from "./runtime-logging.js";
|
|
13
|
-
|
|
14
|
-
"params",
|
|
15
|
-
"secrets",
|
|
16
|
-
"fetch",
|
|
17
|
-
"fs",
|
|
18
|
-
"env",
|
|
19
|
-
"diagnostics",
|
|
20
|
-
"progress",
|
|
21
|
-
"runtimeOptions",
|
|
22
|
-
"root"
|
|
23
|
-
]);
|
|
24
|
-
const RESERVED_SERVICE_NAMES_MESSAGE = "Available reserved names: params, secrets, fetch, fs, env, diagnostics, progress, runtimeOptions, root.";
|
|
12
|
+
import { createEnv, createFs, validateServices } from "./runtime/io.js";
|
|
25
13
|
function splitWords(value) {
|
|
26
14
|
const words = [];
|
|
27
15
|
let current = "";
|
|
@@ -73,40 +61,6 @@ function isOptional(schema) {
|
|
|
73
61
|
function isPlainObject(value) {
|
|
74
62
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
75
63
|
}
|
|
76
|
-
function createFs() {
|
|
77
|
-
return {
|
|
78
|
-
readFile: async (path, encoding = "utf8") => readFile(path, { encoding }),
|
|
79
|
-
writeFile: async (path, contents, options) => {
|
|
80
|
-
await writeFile(path, contents, options);
|
|
81
|
-
},
|
|
82
|
-
exists: async (path) => {
|
|
83
|
-
try {
|
|
84
|
-
await access(path);
|
|
85
|
-
return true;
|
|
86
|
-
}
|
|
87
|
-
catch {
|
|
88
|
-
return false;
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
lstat: async (path) => lstat(path),
|
|
92
|
-
rename: async (fromPath, toPath) => rename(fromPath, toPath),
|
|
93
|
-
unlink: async (path) => unlink(path)
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
function createEnv(values = process.env) {
|
|
97
|
-
return {
|
|
98
|
-
get(key) {
|
|
99
|
-
return values[key];
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
function validateServices(services) {
|
|
104
|
-
for (const name of Object.keys(services)) {
|
|
105
|
-
if (RESERVED_SERVICE_NAMES.has(name)) {
|
|
106
|
-
throw new Error(`Service name "${name}" is reserved. Choose a different name. ${RESERVED_SERVICE_NAMES_MESSAGE}`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
64
|
function formatAvailableList(values) {
|
|
111
65
|
return `Available: ${[...values].sort().join(", ")}.`;
|
|
112
66
|
}
|
|
@@ -260,7 +214,7 @@ function validateArrayConstraints(schema, value, label, errors) {
|
|
|
260
214
|
});
|
|
261
215
|
}
|
|
262
216
|
}
|
|
263
|
-
function validateObjectSchema(schema, value, label, errors) {
|
|
217
|
+
export function validateObjectSchema(schema, value, label, errors) {
|
|
264
218
|
if (!isPlainObject(value)) {
|
|
265
219
|
errors.push({
|
|
266
220
|
path: label,
|
|
@@ -381,22 +335,23 @@ function createResolvedSDK(root, options = {}) {
|
|
|
381
335
|
let secrets;
|
|
382
336
|
let validatedParams;
|
|
383
337
|
try {
|
|
384
|
-
secrets = resolveCommandSecrets(node);
|
|
338
|
+
secrets = resolveCommandSecrets(node, options.env);
|
|
385
339
|
const baseContext = {
|
|
386
340
|
...services,
|
|
387
341
|
runtimeOptions,
|
|
388
342
|
root,
|
|
389
343
|
secrets,
|
|
390
344
|
fetch: runtimeFetch,
|
|
391
|
-
fs: createFs(),
|
|
392
|
-
env: createEnv(),
|
|
345
|
+
fs: createFs(options.fs),
|
|
346
|
+
env: createEnv(options.env),
|
|
393
347
|
diagnostics,
|
|
394
348
|
progress(message) {
|
|
395
349
|
diagnostics.emit({ level: "info", message, category: "progress" });
|
|
396
350
|
}
|
|
397
351
|
};
|
|
398
352
|
await assertCommandRequirements(node, { ...baseContext, params: undefined }, {
|
|
399
|
-
apiVersion: options.apiVersion
|
|
353
|
+
apiVersion: options.apiVersion,
|
|
354
|
+
env: options.env
|
|
400
355
|
});
|
|
401
356
|
const paramsSchema = filterSchemaForScope(node.params, "sdk");
|
|
402
357
|
if (paramsSchema === undefined || paramsSchema.kind !== "object") {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@poe-code/task-list";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "@poe-code/task-list";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export interface ServiceCall {
|
|
2
|
+
method: string;
|
|
3
|
+
args: unknown[];
|
|
4
|
+
result?: unknown;
|
|
5
|
+
error?: unknown;
|
|
6
|
+
}
|
|
7
|
+
export interface FetchRoute {
|
|
8
|
+
method?: string;
|
|
9
|
+
url: string | ((url: string) => boolean);
|
|
10
|
+
status?: number;
|
|
11
|
+
json?: unknown;
|
|
12
|
+
text?: string;
|
|
13
|
+
error?: Error;
|
|
14
|
+
}
|
|
15
|
+
export declare function fakeService<T extends object>(stubs?: Partial<T>): T & {
|
|
16
|
+
calls: ServiceCall[];
|
|
17
|
+
};
|
|
18
|
+
export declare function fakeFetch(routes: FetchRoute[]): typeof globalThis.fetch & {
|
|
19
|
+
calls: Request[];
|
|
20
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
function isPromiseLike(value) {
|
|
2
|
+
return (((typeof value === "object" && value !== null) || typeof value === "function") &&
|
|
3
|
+
typeof value.then === "function");
|
|
4
|
+
}
|
|
5
|
+
export function fakeService(stubs = {}) {
|
|
6
|
+
const calls = [];
|
|
7
|
+
return new Proxy(stubs, {
|
|
8
|
+
get(target, property, receiver) {
|
|
9
|
+
if (property === "calls") {
|
|
10
|
+
return calls;
|
|
11
|
+
}
|
|
12
|
+
const stub = Reflect.get(target, property, receiver);
|
|
13
|
+
if (typeof stub !== "function") {
|
|
14
|
+
if (stub !== undefined) {
|
|
15
|
+
return stub;
|
|
16
|
+
}
|
|
17
|
+
return (...args) => {
|
|
18
|
+
const method = String(property);
|
|
19
|
+
const error = new Error(`Unstubbed service method "${method}" was called.`);
|
|
20
|
+
calls.push({ method, args, error });
|
|
21
|
+
throw error;
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
return (...args) => {
|
|
25
|
+
const method = String(property);
|
|
26
|
+
const call = { method, args };
|
|
27
|
+
calls.push(call);
|
|
28
|
+
try {
|
|
29
|
+
const result = Reflect.apply(stub, receiver, args);
|
|
30
|
+
if (!isPromiseLike(result)) {
|
|
31
|
+
call.result = result;
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
return Promise.resolve(result).then((value) => {
|
|
35
|
+
call.result = value;
|
|
36
|
+
return value;
|
|
37
|
+
}, (error) => {
|
|
38
|
+
call.error = error;
|
|
39
|
+
throw error;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
call.error = error;
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function routeDescription(route) {
|
|
51
|
+
const method = route.method?.toUpperCase() ?? "*";
|
|
52
|
+
const url = typeof route.url === "string" ? route.url : "<predicate>";
|
|
53
|
+
return `${method} ${url}`;
|
|
54
|
+
}
|
|
55
|
+
function routeMatches(route, request) {
|
|
56
|
+
if (route.method !== undefined && route.method.toUpperCase() !== request.method.toUpperCase()) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return typeof route.url === "string" ? route.url === request.url : route.url(request.url);
|
|
60
|
+
}
|
|
61
|
+
export function fakeFetch(routes) {
|
|
62
|
+
const calls = [];
|
|
63
|
+
const fetch = async (input, init) => {
|
|
64
|
+
const request = new Request(input, init);
|
|
65
|
+
calls.push(request);
|
|
66
|
+
const route = routes.find((candidate) => routeMatches(candidate, request));
|
|
67
|
+
if (route === undefined) {
|
|
68
|
+
const configuredRoutes = routes.map(routeDescription).join(", ") || "none";
|
|
69
|
+
throw new Error(`No fake fetch route matched ${request.method} ${request.url}. Configured routes: ${configuredRoutes}.`);
|
|
70
|
+
}
|
|
71
|
+
if (route.error !== undefined) {
|
|
72
|
+
throw route.error;
|
|
73
|
+
}
|
|
74
|
+
if (Object.hasOwn(route, "json")) {
|
|
75
|
+
return new Response(JSON.stringify(route.json), {
|
|
76
|
+
status: route.status ?? 200,
|
|
77
|
+
headers: { "content-type": "application/json" }
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return new Response(route.text ?? null, { status: route.status ?? 200 });
|
|
81
|
+
};
|
|
82
|
+
return Object.assign(fetch, { calls });
|
|
83
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Group } from "../index.js";
|
|
2
|
+
export interface FixtureService {
|
|
3
|
+
execute(value: string): Promise<string>;
|
|
4
|
+
}
|
|
5
|
+
export interface FixtureServices {
|
|
6
|
+
fakeService: FixtureService;
|
|
7
|
+
}
|
|
8
|
+
export declare function createHarnessFixtureGroup(): Group<FixtureServices>;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { S } from "toolcraft-schema";
|
|
2
|
+
import { NotFoundError, UserError, defineCommand, defineGroup } from "../index.js";
|
|
3
|
+
const emptyParams = S.Object({});
|
|
4
|
+
function defineFixtureCommand(config) {
|
|
5
|
+
return defineCommand(config);
|
|
6
|
+
}
|
|
7
|
+
export function createHarnessFixtureGroup() {
|
|
8
|
+
const params = defineFixtureCommand({
|
|
9
|
+
name: "params",
|
|
10
|
+
params: S.Object({
|
|
11
|
+
name: S.String(),
|
|
12
|
+
count: S.Optional(S.Number({ default: 2 }))
|
|
13
|
+
}),
|
|
14
|
+
handler: async ({ params: values }) => values
|
|
15
|
+
});
|
|
16
|
+
const secrets = defineFixtureCommand({
|
|
17
|
+
name: "secrets",
|
|
18
|
+
params: emptyParams,
|
|
19
|
+
secrets: {
|
|
20
|
+
required: { env: "FIXTURE_REQUIRED_SECRET" },
|
|
21
|
+
optional: { env: "FIXTURE_OPTIONAL_SECRET", optional: true }
|
|
22
|
+
},
|
|
23
|
+
handler: async ({ secrets: values }) => values
|
|
24
|
+
});
|
|
25
|
+
const otherSecrets = defineFixtureCommand({
|
|
26
|
+
name: "other-secrets",
|
|
27
|
+
params: emptyParams,
|
|
28
|
+
secrets: {
|
|
29
|
+
required: { env: "FIXTURE_OTHER_REQUIRED_SECRET" }
|
|
30
|
+
},
|
|
31
|
+
handler: async ({ secrets: values }) => values
|
|
32
|
+
});
|
|
33
|
+
const auth = defineFixtureCommand({
|
|
34
|
+
name: "auth",
|
|
35
|
+
params: emptyParams,
|
|
36
|
+
requires: { auth: true },
|
|
37
|
+
handler: async () => "authenticated"
|
|
38
|
+
});
|
|
39
|
+
const check = defineFixtureCommand({
|
|
40
|
+
name: "check",
|
|
41
|
+
params: emptyParams,
|
|
42
|
+
requires: {
|
|
43
|
+
check: async () => ({ ok: false, message: "Fixture check failed." })
|
|
44
|
+
},
|
|
45
|
+
handler: async () => "checked"
|
|
46
|
+
});
|
|
47
|
+
const confirm = defineFixtureCommand({
|
|
48
|
+
name: "confirm",
|
|
49
|
+
params: emptyParams,
|
|
50
|
+
confirm: true,
|
|
51
|
+
handler: async () => "confirmed"
|
|
52
|
+
});
|
|
53
|
+
const humanInLoop = defineFixtureCommand({
|
|
54
|
+
name: "human-in-loop",
|
|
55
|
+
params: S.Object({ target: S.String() }),
|
|
56
|
+
humanInLoop: {
|
|
57
|
+
mode: "sync",
|
|
58
|
+
message: ({ params: values }) => `Deploy ${values.target}?`
|
|
59
|
+
},
|
|
60
|
+
handler: async () => "deployed"
|
|
61
|
+
});
|
|
62
|
+
const humanInLoopPath = defineFixtureCommand({
|
|
63
|
+
name: "human-in-loop-path",
|
|
64
|
+
params: emptyParams,
|
|
65
|
+
humanInLoop: {
|
|
66
|
+
mode: "sync",
|
|
67
|
+
message: ({ commandPath }) => `Approve ${commandPath}?`
|
|
68
|
+
},
|
|
69
|
+
handler: async () => "approved"
|
|
70
|
+
});
|
|
71
|
+
const asyncHumanInLoop = defineFixtureCommand({
|
|
72
|
+
name: "async-human-in-loop",
|
|
73
|
+
params: S.Object({ target: S.String() }),
|
|
74
|
+
humanInLoop: {
|
|
75
|
+
mode: "async",
|
|
76
|
+
message: ({ params: values }) => `Queue ${values.target}?`
|
|
77
|
+
},
|
|
78
|
+
handler: async () => "deployed"
|
|
79
|
+
});
|
|
80
|
+
const rich = defineFixtureCommand({
|
|
81
|
+
name: "rich",
|
|
82
|
+
params: emptyParams,
|
|
83
|
+
handler: async () => ({ value: "rich" }),
|
|
84
|
+
render: {
|
|
85
|
+
rich: (result, primitives) => {
|
|
86
|
+
primitives.logger.info(`\u001b[31m${result.value}\u001b[0m`);
|
|
87
|
+
primitives.logger.info(primitives.getTheme().intro("intro"));
|
|
88
|
+
primitives.logger.info(primitives.renderTable({
|
|
89
|
+
theme: primitives.getTheme(),
|
|
90
|
+
variant: "detail",
|
|
91
|
+
maxWidth: 24,
|
|
92
|
+
columns: [
|
|
93
|
+
{ name: "label", title: "Label", alignment: "left", maxLen: 11 },
|
|
94
|
+
{ name: "value", title: "Value", alignment: "left", maxLen: 8 }
|
|
95
|
+
],
|
|
96
|
+
rows: [
|
|
97
|
+
{
|
|
98
|
+
label: "Description",
|
|
99
|
+
value: "A deterministic renderer wraps this detail using a fixed eighty-column width."
|
|
100
|
+
}
|
|
101
|
+
]
|
|
102
|
+
}));
|
|
103
|
+
primitives.note("captured note", "Capture");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
const markdown = defineFixtureCommand({
|
|
108
|
+
name: "markdown",
|
|
109
|
+
params: emptyParams,
|
|
110
|
+
handler: async () => ({ value: "markdown" }),
|
|
111
|
+
render: {
|
|
112
|
+
markdown: (result, primitives) => primitives.getTheme().header(`# ${result.value}`)
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const json = defineFixtureCommand({
|
|
116
|
+
name: "json",
|
|
117
|
+
params: emptyParams,
|
|
118
|
+
handler: async () => ({ value: "json" }),
|
|
119
|
+
render: {
|
|
120
|
+
json: (result, primitives) => ({ value: primitives.getTheme().success(result.value) })
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
const renderError = defineFixtureCommand({
|
|
124
|
+
name: "render-error",
|
|
125
|
+
params: emptyParams,
|
|
126
|
+
handler: async () => "handled",
|
|
127
|
+
render: {
|
|
128
|
+
rich: (_result, primitives) => primitives.logger.info("rendered before failure"),
|
|
129
|
+
markdown: () => {
|
|
130
|
+
throw new Error("Fixture renderer failed.");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
const service = defineFixtureCommand({
|
|
135
|
+
name: "service",
|
|
136
|
+
params: S.Object({ value: S.String() }),
|
|
137
|
+
handler: async ({ fakeService, params: values }) => fakeService.execute(values.value)
|
|
138
|
+
});
|
|
139
|
+
const fs = defineFixtureCommand({
|
|
140
|
+
name: "fs",
|
|
141
|
+
params: emptyParams,
|
|
142
|
+
handler: async ({ fs: handlerFs }) => {
|
|
143
|
+
await handlerFs.writeFile("/result.txt", "written");
|
|
144
|
+
return "written";
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
const fsRoundTrip = defineFixtureCommand({
|
|
148
|
+
name: "fs-roundtrip",
|
|
149
|
+
params: emptyParams,
|
|
150
|
+
handler: async ({ fs: handlerFs }) => {
|
|
151
|
+
await handlerFs.writeFile("/roundtrip.txt", "written");
|
|
152
|
+
return handlerFs.readFile("/roundtrip.txt");
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
const effects = defineFixtureCommand({
|
|
156
|
+
name: "effects",
|
|
157
|
+
params: emptyParams,
|
|
158
|
+
handler: async (context) => {
|
|
159
|
+
context.diagnostics.emit({ level: "debug", message: "starting effects" });
|
|
160
|
+
const envValue = context.env.get("FIXTURE_VALUE");
|
|
161
|
+
context.progress("halfway");
|
|
162
|
+
const serviceValue = await context.fakeService.execute(envValue ?? "missing");
|
|
163
|
+
const response = await context.fetch("https://fixture.test/value", { method: "POST" });
|
|
164
|
+
await context.fs.writeFile("/effects.txt", await response.text());
|
|
165
|
+
return serviceValue;
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
const alias = defineFixtureCommand({
|
|
169
|
+
name: "canonical",
|
|
170
|
+
aliases: ["alias"],
|
|
171
|
+
params: emptyParams,
|
|
172
|
+
handler: async () => "alias"
|
|
173
|
+
});
|
|
174
|
+
const hidden = defineFixtureCommand({
|
|
175
|
+
name: "hidden",
|
|
176
|
+
hidden: true,
|
|
177
|
+
params: emptyParams,
|
|
178
|
+
handler: async () => "hidden"
|
|
179
|
+
});
|
|
180
|
+
const defaultCommand = defineFixtureCommand({
|
|
181
|
+
name: "show",
|
|
182
|
+
params: emptyParams,
|
|
183
|
+
handler: async () => "default"
|
|
184
|
+
});
|
|
185
|
+
const nested = defineGroup({
|
|
186
|
+
name: "nested",
|
|
187
|
+
children: [defaultCommand],
|
|
188
|
+
default: defaultCommand
|
|
189
|
+
});
|
|
190
|
+
const deferred = defineGroup({
|
|
191
|
+
name: "deferred",
|
|
192
|
+
mcp: { transport: "stdio", command: "fixture-server" },
|
|
193
|
+
children: []
|
|
194
|
+
});
|
|
195
|
+
const userError = defineFixtureCommand({
|
|
196
|
+
name: "user-error",
|
|
197
|
+
params: emptyParams,
|
|
198
|
+
handler: async () => {
|
|
199
|
+
throw new UserError("Fixture user error.");
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
const notFound = defineFixtureCommand({
|
|
203
|
+
name: "not-found",
|
|
204
|
+
params: emptyParams,
|
|
205
|
+
handler: async () => {
|
|
206
|
+
throw new NotFoundError({
|
|
207
|
+
request: { method: "GET", url: "https://fixture.test/missing", headers: {} },
|
|
208
|
+
response: { status: 404, statusText: "Not Found", headers: {}, body: null }
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
const plainError = defineFixtureCommand({
|
|
213
|
+
name: "plain-error",
|
|
214
|
+
params: emptyParams,
|
|
215
|
+
handler: async () => {
|
|
216
|
+
throw new Error("Fixture plain error.");
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
return defineGroup({
|
|
220
|
+
name: "fixture",
|
|
221
|
+
children: [
|
|
222
|
+
params,
|
|
223
|
+
secrets,
|
|
224
|
+
otherSecrets,
|
|
225
|
+
auth,
|
|
226
|
+
check,
|
|
227
|
+
confirm,
|
|
228
|
+
humanInLoop,
|
|
229
|
+
humanInLoopPath,
|
|
230
|
+
asyncHumanInLoop,
|
|
231
|
+
rich,
|
|
232
|
+
markdown,
|
|
233
|
+
json,
|
|
234
|
+
renderError,
|
|
235
|
+
service,
|
|
236
|
+
fs,
|
|
237
|
+
fsRoundTrip,
|
|
238
|
+
effects,
|
|
239
|
+
alias,
|
|
240
|
+
hidden,
|
|
241
|
+
nested,
|
|
242
|
+
deferred,
|
|
243
|
+
userError,
|
|
244
|
+
notFound,
|
|
245
|
+
plainError
|
|
246
|
+
]
|
|
247
|
+
});
|
|
248
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { type Group, type HandlerFs } from "../index.js";
|
|
2
|
+
import { type DiagnosticLogEvent, type LogLevel } from "../runtime-logging.js";
|
|
3
|
+
import { type FetchRoute } from "./fakes.js";
|
|
4
|
+
import { type FsChange, type MemoryFs } from "./memory-fs.js";
|
|
5
|
+
import { type ParityResult } from "./parity.js";
|
|
6
|
+
export type PipelineStage = "resolve" | "secrets" | "requirements" | "params" | "confirm" | "handler" | "render";
|
|
7
|
+
export type EffectEvent = {
|
|
8
|
+
seq: number;
|
|
9
|
+
kind: "fetch";
|
|
10
|
+
method: string;
|
|
11
|
+
url: string;
|
|
12
|
+
} | {
|
|
13
|
+
seq: number;
|
|
14
|
+
kind: "fs";
|
|
15
|
+
op: "writeFile" | "rename" | "unlink";
|
|
16
|
+
path: string;
|
|
17
|
+
} | {
|
|
18
|
+
seq: number;
|
|
19
|
+
kind: "service";
|
|
20
|
+
service: string;
|
|
21
|
+
method: string;
|
|
22
|
+
args: unknown[];
|
|
23
|
+
} | {
|
|
24
|
+
seq: number;
|
|
25
|
+
kind: "env";
|
|
26
|
+
key: string;
|
|
27
|
+
} | {
|
|
28
|
+
seq: number;
|
|
29
|
+
kind: "progress";
|
|
30
|
+
message: string;
|
|
31
|
+
} | {
|
|
32
|
+
seq: number;
|
|
33
|
+
kind: "confirm";
|
|
34
|
+
message: string;
|
|
35
|
+
approved: boolean;
|
|
36
|
+
};
|
|
37
|
+
export interface ConfirmationRequest {
|
|
38
|
+
message: string;
|
|
39
|
+
declineInputPrompt?: string;
|
|
40
|
+
}
|
|
41
|
+
export interface HarnessOptions<TServices extends object> {
|
|
42
|
+
services?: TServices;
|
|
43
|
+
env?: Record<string, string | undefined>;
|
|
44
|
+
secrets?: Record<string, string>;
|
|
45
|
+
fs?: Record<string, string> | HandlerFs;
|
|
46
|
+
fetch?: typeof globalThis.fetch | FetchRoute[];
|
|
47
|
+
confirmations?: "approve" | "decline" | ((request: ConfirmationRequest) => boolean | Promise<boolean>);
|
|
48
|
+
apiVersion?: string;
|
|
49
|
+
logLevel?: LogLevel;
|
|
50
|
+
}
|
|
51
|
+
export interface RunResult<T> {
|
|
52
|
+
ok: boolean;
|
|
53
|
+
value?: T;
|
|
54
|
+
error?: unknown;
|
|
55
|
+
failedAt?: PipelineStage;
|
|
56
|
+
pending: boolean;
|
|
57
|
+
logs: DiagnosticLogEvent[];
|
|
58
|
+
progress: string[];
|
|
59
|
+
confirmations: ConfirmationRequest[];
|
|
60
|
+
timeline: EffectEvent[];
|
|
61
|
+
fsChanges: FsChange[];
|
|
62
|
+
rendered: {
|
|
63
|
+
rich?: string;
|
|
64
|
+
markdown?: string;
|
|
65
|
+
json?: unknown;
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
export interface CommandTestHarness {
|
|
69
|
+
run<T>(path: string[], params?: Record<string, unknown>): Promise<RunResult<T>>;
|
|
70
|
+
parity(path: string[], params?: Record<string, unknown>): Promise<ParityResult>;
|
|
71
|
+
fs: MemoryFs;
|
|
72
|
+
timeline: EffectEvent[];
|
|
73
|
+
}
|
|
74
|
+
type EmptyHarnessServices = Record<string, never>;
|
|
75
|
+
export declare function createCommandTestHarness<TServices extends object = EmptyHarnessServices>(root: Group, options?: HarnessOptions<TServices>): CommandTestHarness;
|
|
76
|
+
export {};
|