feat: implement .apply REPL dot-command (Approach 1: Minimalist Function Injection)#1789
Conversation
pmcelhaney
left a comment
There was a problem hiding this comment.
Add documentation and a changeset
Head branch was pushed to by a user without write access
Added a "Scenario scripts with |
pmcelhaney
left a comment
There was a problem hiding this comment.
The code generator should create a default ./scenarios/index.ts file if one doesn't already exist.
It should import the ApplyContext type and have some commented out example code to demonstrate how to use it.
| /** Create a new route builder for a given path */ | ||
| route: (path: string) => unknown; | ||
| } | ||
| `; |
There was a problem hiding this comment.
ApplyContext should import the Context class:
// This file is generated by Counterfact. Do not edit manually.
import type { Context } from "../routes/_.context";
export interface ApplyContext {
/** Root context, same as loadContext("/") */
context: Context;
/** Load a context object for a specific path */
loadContext: (path: string) => Record<string, unknown>;
/** Named route builders stored in the REPL execution context */
routes: Record<string, unknown>;
/** Create a new route builder for a given path */
route: (path: string) => unknown;
}There was a problem hiding this comment.
Updated in commit 3bdc527. ApplyContext now imports Context from ../routes/_.context and uses it as the type for the context field.
…enarios/index.ts scaffold Agent-Logs-Url: https://github.com/counterfact/api-simulator/sessions/979d2099-b16b-4dce-9b48-6eca562985a4
Done in commit |
pmcelhaney
left a comment
There was a problem hiding this comment.
The apply context type file should also look for _.context.ts files in subdirectories.
If found it should import them and use them to narrow the type of loadContext().
This means that the file will need to be regenerate whenever a _.context.ts file is added or removed.
pmcelhaney
left a comment
There was a problem hiding this comment.
scenario-context.ts should be rewritten when a context is added to or removed from the context registry
Done in commits
|
pmcelhaney
left a comment
There was a problem hiding this comment.
ModuleLoader is an EventTarget. Use an event instead of a callback.
…changed Agent-Logs-Url: https://github.com/counterfact/api-simulator/sessions/a04091de-235d-4f67-bc2b-f8e77f609a7a Co-authored-by: pmcelhaney <51504+pmcelhaney@users.noreply.github.com>
Done in commit |
pmcelhaney
left a comment
There was a problem hiding this comment.
Actually, ContextRegistry should be an EventTarget. It should listen to an event on ContextRegistry rather than ModuleLoader.
Done in commit |
Summary
Implements
.applyas a REPL dot-command that dynamically imports a TypeScript/JavaScript scenario file and calls a named export, injecting the live REPL environment (context,loadContext,routes,route) as its argument.Path resolution:
.apply fooscenarios/index.tsfoo.apply foo/barscenarios/foo.tsbar.apply foo/bar/bazscenarios/foo/bar.tsbazExample scenario script:
Then in the REPL:
.apply soldPetsThe
ApplyContextinterface andScenariotype are generated intotypes/scenario-context.ts.ApplyContextreferences the generatedContextclass fromroutes/_.context, giving scenario scripts full type safety against the user's own context shape.Scenariois defined as($: ApplyContext) => Promise<void> | void, so scenario functions can be typed with a single import. Additionally,loadContext()is typed with narrowed overloads for every_.context.tsfile found in subdirectories ofroutes/, so calls likeloadContext("/pets")return the specificPetsContexttype rather thanRecord<string, unknown>. Overloads are sorted deepest-first so TypeScript resolves the most specific type. Each{param}segment in a route path is individually replaced with${string}(e.g./pets/{petId}/info→`/pets/${string}/info`), preserving full route shape for precise overload resolution. Thescenario-context.tsfile is always regenerated at startup and also regenerated at runtime whenever a_.context.tsfile is added or removed while the server is running —ContextRegistry(which extendsEventTarget) dispatches a"context-changed"event onadd()andremove(), andapp.tslistens for it to callwriteApplyContextType, soloadContextoverloads stay in sync with the live context registry.Code generation scaffolds a
scenarios/index.tsfile (only when it does not already exist) using JSDoc comments,$as the parameter name typed viaScenario, and a concretehelparrow-function example (export const help: Scenario = ($) => { ... };) that users can immediately call with.apply help. The scaffold includes avoid $;statement to suppress unused-variable warnings and JSDoc blocks explaining common usage patterns for$.context,$.loadContext, and$.routes.Scenario files are loaded by
ModuleLoaderinto a newScenarioRegistry(modeled afterRegistryandContextRegistry), which is then passed to the REPL. The REPL no longer does any file I/O — the.applycommand and tab completion look up modules fromScenarioRegistrydirectly.ScenarioRegistrystores modules keyed by slash-delimited relative file path (e.g."index","myscript","foo/bar") and exposesgetModule,getExportedFunctionNames, andgetFileKeys. Declaration files (.d.ts) and source maps (.map) are excluded from scenario loading to prevent noisy runtime errors.The REPL completer supports tab completion for
.apply: typing.apply sol<Tab>completes to exported function names fromscenarios/index.tsmatchingsol; file-prefix completion (e.g.myscript/) is also supported for multi-file scenarios. Only exported functions (notconst,let,var, orclassexports) appear as completions.Original Prompt
Implement
.applyas a REPL dot-command that dynamically imports a TypeScript/JavaScript scenario file and calls a named export, injecting the live REPL environment as its argument.Manual acceptance tests
.apply helpin the REPL prints the help message about scenarios.applyshows exported function names fromscenarios/index.ts_.context.tsfile to a routes subdirectory while the server is running causestypes/scenario-context.tsto be rewritten with new narrowedloadContextoverloads_.context.tsfile at runtime also triggers regeneration oftypes/scenario-context.tstypes/scenario-context.tsandscenarios/index.ts(when not already present)Tasks
.applyREPL dot-command with path-based file/function resolutiontypes/scenario-context.tswithApplyContextinterface (importingContextfromroutes/_.context), narrowedloadContextoverloads for each_.context.tssubdirectory, andScenariotype aliasContextRegistryextendEventTargetand dispatch"context-changed"onadd()andremove();app.tslistens oncontextRegistryto regeneratescenario-context.tsat runtimescenarios/index.ts(only when absent) with JSDoc comments and a runnablehelpexample usingexport const help: Scenario = ($) => { ... }ScenarioRegistry(src/server/scenario-registry.ts) to store scenario modules keyed by relative file path;ModuleLoaderloads and watches the scenarios directory, excluding.d.tsand.mapfiles.apply-aware tab completion to the REPL completer (exported functions only)apply-context.tstoscenario-context.tsthroughoutDirentimported fromnode:fsinstead ofnode:fs/promisesdocs/usage.mdand a changeset