Skip to content

feat: implement .apply REPL dot-command (Approach 1: Minimalist Function Injection)#1789

Open
Copilot wants to merge 15 commits intomainfrom
copilot/implement-apply-command-design
Open

feat: implement .apply REPL dot-command (Approach 1: Minimalist Function Injection)#1789
Copilot wants to merge 15 commits intomainfrom
copilot/implement-apply-command-design

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 8, 2026

Summary

Implements .apply as 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:

Command File Function
.apply foo scenarios/index.ts foo
.apply foo/bar scenarios/foo.ts bar
.apply foo/bar/baz scenarios/foo/bar.ts baz

Example scenario script:

// scenarios/index.ts
import type { ApplyContext } from "../types/apply-context.js";

export function soldPets(ctx: ApplyContext) {
  ctx.context.petService.reset();
  ctx.routes.findSold = ctx.route("/pet/findByStatus").method("get").query({ status: "sold" });
}

Then in the REPL: .apply soldPets

The ApplyContext type references the generated Context class from routes/_.context, giving scenario scripts full type safety against the user's own context shape. Additionally, loadContext() is typed with narrowed overloads for every _.context.ts file found in subdirectories of routes/, so calls like loadContext("/pets") return the specific PetsContext type rather than Record<string, unknown>. Overloads are sorted deepest-first so TypeScript resolves the most specific type. The apply-context.ts file is always regenerated, so adding or removing a _.context.ts in any subdirectory is automatically reflected on the next generation run.

Code generation also scaffolds a scenarios/index.ts file (only when it does not already exist) that imports ApplyContext and includes generic commented-out example code covering context mutation, loadContext, and route builder storage.

Scenario files are loaded by ModuleLoader into a new ScenarioRegistry (modeled after Registry and ContextRegistry), which is then passed to the REPL. The REPL no longer does any file I/O — the .apply command and tab completion look up modules from ScenarioRegistry directly. ScenarioRegistry stores modules keyed by slash-delimited relative file path (e.g. "index", "myscript", "foo/bar") and exposes getModule, getExportedFunctionNames, and getFileKeys.

The REPL completer supports tab completion for .apply: typing .apply <Tab> suggests exported function names from the "index" scenario and file-key prefixes (e.g. myscript/); typing .apply myscript/<Tab> suggests exported function names from the "myscript" scenario as myscript/<name>. Only exported functions are included — const, let, var, and class exports are excluded.

Original Prompt

Implement .apply as a REPL dot-command (Approach 1: Minimalist Function Injection). The command dynamically imports a TypeScript/JavaScript scenario file and calls a named export, injecting the live REPL environment as its argument.

Manual acceptance tests

  • .apply foo calls foo(ctx) from scenarios/index.ts, printing Applied foo and mutating context as expected
  • .apply foo/bar calls bar(ctx) from scenarios/foo.ts; .apply foo/bar/baz calls baz(ctx) from scenarios/foo/bar.ts
  • Named entries written to ctx.routes inside a script are accessible as routes.myName in the REPL after the command completes
  • Running .apply missing (no matching scenario in registry) prints a descriptive error message
  • After first-time code generation, scenarios/index.ts exists with an ApplyContext import and commented-out generic example code; re-running code generation does not overwrite it
  • With routes/pets/_.context.ts present, loadContext("/pets") is typed to return PetsContext; deeper overloads appear before shallower ones in the generated apply-context.ts
  • Tab-completing .apply <Tab> shows only exported function names and file-key prefixes; .apply myscript/<Tab> shows exported function names as myscript/<name> — non-function exports (const, class, etc.) are excluded

Tasks

  • src/server/scenario-registry.ts (new): ScenarioRegistry class modeled after Registry and ContextRegistry. Stores scenario modules keyed by slash-delimited relative file path. Provides add, remove, getModule, getExportedFunctionNames (returns only function-valued exports), and getFileKeys.
  • src/server/module-loader.ts: Extended with optional scenariosPath and scenarioRegistry constructor params. load() scans the scenarios directory and populates ScenarioRegistry; watch() keeps it up to date on file changes. A missing scenarios directory is handled gracefully.
  • src/repl/repl.ts: Added .apply command with path traversal guard (rejects ../. segments); looks up module via scenarioRegistry.getModule(fileKey) and calls the named export with an ApplyContext-shaped object. Removed all file-system helpers (getExportedNames, getScenarioFilePrefixes, getApplyCompletions). createCompleter() now accepts ScenarioRegistry instead of scenariosDir; tab completions are derived from registry data. Added JSDoc to createCompleter.
  • src/app.ts: Creates ScenarioRegistry, passes it to ModuleLoader (with scenarios/ path) and to startRepl.
  • src/typescript-generator/generate.ts: Added collectContextFiles(), buildApplyContextContent(), routePathToAlias(), writeDefaultScenariosIndex(), and writeApplyContextType() for generating types/apply-context.ts with narrowed loadContext overloads and scaffolding scenarios/index.ts.
  • test/server/scenario-registry.test.ts (new): Unit tests for ScenarioRegistry covering add, remove, getModule, getExportedFunctionNames (functions only), getFileKeys, and overwrite behaviour.
  • test/server/module-loader.test.ts: Added 3 scenario-loading tests: populating registry on load(), nested file keys, and graceful handling of a missing scenarios directory.
  • test/repl/repl.test.ts: Updated .apply tests to use pre-populated ScenarioRegistry (no temporary files); updated tab completion tests to use ScenarioRegistry; all usingTemporaryFiles usage removed from .apply and completion tests.
  • test/typescript-generator/generate.test.ts: Added 4 tests for apply-context.ts generation covering fallback types, root-only context, root + subdirectory context (overload order), and parameterised path context.
  • .changeset/apply-command.md: Minor version changeset.
  • docs/usage.md: Added "Scenario scripts with .apply" subsection covering path resolution, a full annotated example script, and how to use stored ctx.routes entries from the REPL after applying.

Copilot AI linked an issue Apr 8, 2026 that may be closed by this pull request
8 tasks
@pmcelhaney pmcelhaney marked this pull request as ready for review April 8, 2026 21:41
Copilot AI changed the title [WIP] Implement .apply command design with minimalist function injection feat: implement .apply REPL dot-command (Approach 1: Minimalist Function Injection) Apr 8, 2026
Copilot AI requested a review from pmcelhaney April 8, 2026 21:59
@pmcelhaney pmcelhaney enabled auto-merge April 8, 2026 22:07
Copy link
Copy Markdown
Collaborator

@pmcelhaney pmcelhaney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add documentation and a changeset

auto-merge was automatically disabled April 8, 2026 22:11

Head branch was pushed to by a user without write access

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 8, 2026

Add documentation and a changeset

Added a "Scenario scripts with .apply" section to docs/usage.md (commit ad25c0c). It covers path resolution, a full example script, and how to use ctx.routes after applying. The changeset was already in place from the previous commit.

Copilot AI requested a review from pmcelhaney April 8, 2026 22:12
Copy link
Copy Markdown
Collaborator

@pmcelhaney pmcelhaney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}
`;
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in commit 3bdc527. ApplyContext now imports Context from ../routes/_.context and uses it as the type for the context field.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 8, 2026

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 ...

Done in commit 18a7690. writeDefaultScenariosIndex now runs alongside writeApplyContextType during code generation and writes scenarios/index.ts only when the file doesn't already exist. The scaffold imports ApplyContext and includes generic commented-out example code covering context mutation, loadContext, and route builder storage.

Copilot AI requested a review from pmcelhaney April 8, 2026 22:42
Copy link
Copy Markdown
Collaborator

@pmcelhaney pmcelhaney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

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(). ...

Done in commit dacc7d6 (with follow-up cleanup in 8571e86). writeApplyContextType now walks the routes/ directory recursively to collect every _.context.ts file. For each one it:

  • Computes a unique import alias (PetsContext, PetsPetIdContext, etc.)
  • Generates a narrowed loadContext overload (e.g., loadContext(path: "/pets" | \/pets/${string}`): PetsContext`)
  • Sorts overloads deepest-first so TypeScript resolves the most specific context type first

The file is always overwritten on every generation run, so adding or removing a _.context.ts in a subdirectory is automatically reflected the next time the generator runs.

Copilot AI requested a review from pmcelhaney April 9, 2026 12:52
@pmcelhaney
Copy link
Copy Markdown
Collaborator

@copilot in the REPl add tab completion for .apply

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

@copilot in the REPl add tab completion for .apply

Done in commit b08a259. Tab completion for .apply is now wired into createCompleter:

  • .apply sol<Tab> → filters exports from scenarios/index.ts matching sol (e.g. soldPets)
  • .apply <Tab> → shows all exports from scenarios/index.ts plus file prefixes like myscript/
  • .apply myscript/<Tab> → shows all exports from scenarios/myscript.ts as myscript/<name>
  • .apply myscript/sol<Tab> → filters to matching exports

Completions are read by scanning files with a regex for export [async] function/const/let/var/class declarations.

@pmcelhaney
Copy link
Copy Markdown
Collaborator

@copilot the completer should only look for exported functions

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

@copilot the completer should only look for exported functions

Done in commit 0f9bf16. The regex in getExportedNames now matches only export [async] function <name>const, let, var, and class exports are excluded. The test also verifies that export const notAScenario = 42 does not appear in completions.

Copy link
Copy Markdown
Collaborator

@pmcelhaney pmcelhaney left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Create a ScenarioRegistry modeled after Registry and ContextRegistry.

It should not be the REPL's responsibility to load modules from the scenario directory. They should be loaded by the ModuleLoader and added the ScenarioRegistry. The ScenarioRegistry will be passed to the REPL.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 9, 2026

Create a ScenarioRegistry modeled after Registry and ContextRegistry.

It should not be the REPL's responsibility to load modules from the scenario directory. They should be loaded by the ModuleLoade...

Done in commit fb4b293. Created ScenarioRegistry (src/server/scenario-registry.ts) modeled after Registry and ContextRegistry — it stores scenario modules keyed by relative file path (e.g. "index", "myscript", "foo/bar") and provides add, remove, getModule, getExportedFunctionNames, and getFileKeys.

ModuleLoader now accepts optional scenariosPath and scenarioRegistry constructor params and loads/watches the scenarios directory alongside the routes directory. The REPL no longer does any file I/O — the .apply command and tab completion read from ScenarioRegistry directly. app.ts wires everything together by creating a ScenarioRegistry, passing it to ModuleLoader (with the scenarios path), and forwarding it to startRepl.

Copilot AI requested a review from pmcelhaney April 9, 2026 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

.apply Command Design (Approach 1): Minimalist Function Injection

2 participants