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.
Files changed (76) hide show
  1. package/README.md +74 -4
  2. package/composition.json +1 -1
  3. package/dist/agent-defs.d.ts +1 -0
  4. package/dist/agent-defs.js +1 -0
  5. package/dist/agent-human-in-loop.d.ts +1 -0
  6. package/dist/agent-human-in-loop.js +1 -0
  7. package/dist/agent-mcp-config.d.ts +1 -0
  8. package/dist/agent-mcp-config.js +1 -0
  9. package/dist/auth-store.d.ts +1 -0
  10. package/dist/auth-store.js +1 -0
  11. package/dist/cli.d.ts +4 -1
  12. package/dist/cli.js +24 -65
  13. package/dist/composition.json +1 -1
  14. package/dist/config-mutations.d.ts +1 -0
  15. package/dist/config-mutations.js +1 -0
  16. package/dist/frontmatter.d.ts +1 -0
  17. package/dist/frontmatter.js +1 -0
  18. package/dist/human-in-loop/gate.d.ts +5 -1
  19. package/dist/human-in-loop/gate.js +10 -8
  20. package/dist/human-in-loop/runner.js +1 -28
  21. package/dist/mcp.d.ts +4 -1
  22. package/dist/mcp.js +8 -51
  23. package/dist/process-runner.d.ts +1 -0
  24. package/dist/process-runner.js +1 -0
  25. package/dist/runtime/io.d.ts +5 -0
  26. package/dist/runtime/io.js +50 -0
  27. package/dist/sdk.d.ts +12 -8
  28. package/dist/sdk.js +7 -52
  29. package/dist/task-list.d.ts +1 -0
  30. package/dist/task-list.js +1 -0
  31. package/dist/testing/fakes.d.ts +20 -0
  32. package/dist/testing/fakes.js +83 -0
  33. package/dist/testing/fixtures.d.ts +8 -0
  34. package/dist/testing/fixtures.js +248 -0
  35. package/dist/testing/harness.d.ts +76 -0
  36. package/dist/testing/harness.js +391 -0
  37. package/dist/testing/index.d.ts +4 -0
  38. package/dist/testing/index.js +3 -0
  39. package/dist/testing/memory-fs.d.ts +11 -0
  40. package/dist/testing/memory-fs.js +61 -0
  41. package/dist/testing/parity.d.ts +25 -0
  42. package/dist/testing/parity.js +384 -0
  43. package/dist/testing/render-capture.d.ts +6 -0
  44. package/dist/testing/render-capture.js +54 -0
  45. package/dist/tiny-mcp-client.d.ts +1 -0
  46. package/dist/tiny-mcp-client.js +1 -0
  47. package/node_modules/@poe-code/agent-defs/package.json +2 -0
  48. package/node_modules/@poe-code/agent-human-in-loop/README.md +12 -2
  49. package/node_modules/@poe-code/agent-human-in-loop/package.json +2 -0
  50. package/node_modules/@poe-code/agent-mcp-config/README.md +7 -7
  51. package/node_modules/@poe-code/agent-mcp-config/package.json +2 -0
  52. package/node_modules/@poe-code/config-mutations/README.md +8 -8
  53. package/node_modules/@poe-code/config-mutations/package.json +2 -0
  54. package/node_modules/@poe-code/frontmatter/README.md +2 -2
  55. package/node_modules/@poe-code/frontmatter/package.json +2 -0
  56. package/node_modules/@poe-code/process-runner/README.md +1 -1
  57. package/node_modules/@poe-code/process-runner/package.json +2 -0
  58. package/node_modules/@poe-code/task-list/README.md +15 -3
  59. package/node_modules/@poe-code/task-list/package.json +2 -0
  60. package/node_modules/auth-store/README.md +15 -0
  61. package/node_modules/auth-store/package.json +2 -0
  62. package/node_modules/tiny-mcp-client/README.md +36 -36
  63. package/node_modules/tiny-mcp-client/package.json +2 -0
  64. package/node_modules/toolcraft-design/README.md +8 -4
  65. package/node_modules/toolcraft-design/dist/dashboard/terminal.js +48 -6
  66. package/node_modules/toolcraft-design/dist/explorer/actions.d.ts +4 -0
  67. package/node_modules/toolcraft-design/dist/explorer/actions.js +1 -0
  68. package/node_modules/toolcraft-design/dist/explorer/events.d.ts +4 -0
  69. package/node_modules/toolcraft-design/dist/explorer/reducer.js +52 -4
  70. package/node_modules/toolcraft-design/dist/explorer/render/list.js +47 -20
  71. package/node_modules/toolcraft-design/dist/explorer/render/modal.js +13 -1
  72. package/node_modules/toolcraft-design/dist/explorer/render/test-fixtures.js +1 -1
  73. package/node_modules/toolcraft-design/dist/explorer/runtime.js +3 -0
  74. package/node_modules/toolcraft-design/dist/explorer/state.d.ts +9 -0
  75. package/node_modules/toolcraft-design/package.json +5 -0
  76. 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 createSDK<TRootInfo, TServices extends object = Record<string, unknown>>(root: Group<any> & {
76
- readonly __agentKitGroupTypeInfo: TRootInfo;
77
- }, options?: CreateSDKOptions<TServices>): TRootInfo extends {
78
- children: infer TChildren extends readonly unknown[];
79
- } ? SDKChildrenShape<TChildren, undefined, undefined> : EmptyRecord;
80
- export declare function createSDK<TServices extends object = Record<string, unknown>>(root: Group<TServices>, options?: CreateSDKOptions<TServices>): Record<string, unknown>;
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
- const RESERVED_SERVICE_NAMES = new Set([
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 {};