Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions src/lib/init/local-ops.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from "./constants.js";
import type {
ApplyPatchsetPayload,
DirEntry,
FileExistsBatchPayload,
ListDirPayload,
LocalOpPayload,
Expand Down Expand Up @@ -165,6 +166,20 @@ function safePath(cwd: string, relative: string): string {
return resolved;
}

/**
* Pre-compute directory listing before the first API call.
* Uses the same parameters the server's discover-context step would request.
*/
export function precomputeDirListing(directory: string): DirEntry[] {
const result = listDir({
type: "local-op",
operation: "list-dir",
cwd: directory,
params: { path: ".", recursive: true, maxDepth: 3, maxEntries: 500 },
});
return (result.data as { entries?: DirEntry[] })?.entries ?? [];
}

export async function handleLocalOp(
payload: LocalOpPayload,
options: WizardOptions
Expand Down Expand Up @@ -218,11 +233,7 @@ function listDir(payload: ListDirPayload): LocalOpResult {
const maxEntries = params.maxEntries ?? 500;
const recursive = params.recursive ?? false;

const entries: Array<{
name: string;
path: string;
type: "file" | "directory";
}> = [];
const entries: DirEntry[] = [];

// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: walking the directory tree is a complex operation
function walk(dir: string, depth: number): void {
Expand Down
6 changes: 6 additions & 0 deletions src/lib/init/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
export type DirEntry = {
name: string;
path: string;
type: "file" | "directory";
};

export type WizardOptions = {
directory: string;
force: boolean;
Expand Down
21 changes: 16 additions & 5 deletions src/lib/init/wizard-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ import {
} from "./constants.js";
import { formatError, formatResult } from "./formatters.js";
import { handleInteractive } from "./interactive.js";
import { handleLocalOp } from "./local-ops.js";
import { handleLocalOp, precomputeDirListing } from "./local-ops.js";
import type {
InteractivePayload,
LocalOpPayload,
Expand Down Expand Up @@ -50,7 +50,8 @@ function nextPhase(

async function handleSuspendedStep(
ctx: StepContext,
stepPhases: Map<string, number>
stepPhases: Map<string, number>,
stepHistory: Map<string, Record<string, unknown>[]>
): Promise<Record<string, unknown>> {
const { payload, stepId, spin, options } = ctx;
const { type: payloadType, operation } = payload as {
Expand All @@ -65,9 +66,14 @@ async function handleSuspendedStep(

const localResult = await handleLocalOp(payload as LocalOpPayload, options);

const history = stepHistory.get(stepId) ?? [];
history.push(localResult);
stepHistory.set(stepId, history);

return {
...localResult,
_phase: nextPhase(stepPhases, stepId, ["read-files", "analyze", "done"]),
_prevPhases: history.slice(0, -1),
};
}

Expand Down Expand Up @@ -150,13 +156,16 @@ export async function runWizard(options: WizardOptions): Promise<void> {

const spin = spinner();

spin.start("Scanning project...");
const dirListing = precomputeDirListing(directory);

let run: Awaited<ReturnType<typeof workflow.createRun>>;
let result: WorkflowRunResult;
try {
spin.start("Connecting to wizard...");
spin.message("Connecting to wizard...");
run = await workflow.createRun();
result = (await run.startAsync({
inputData: { directory, force, yes, dryRun, features },
inputData: { directory, force, yes, dryRun, features, dirListing },
tracingOptions,
})) as WorkflowRunResult;
} catch (err) {
Expand All @@ -168,6 +177,7 @@ export async function runWizard(options: WizardOptions): Promise<void> {
}

const stepPhases = new Map<string, number>();
const stepHistory = new Map<string, Record<string, unknown>[]>();

try {
while (result.status === "suspended") {
Expand All @@ -185,7 +195,8 @@ export async function runWizard(options: WizardOptions): Promise<void> {

const resumeData = await handleSuspendedStep(
{ payload: extracted.payload, stepId: extracted.stepId, spin, options },
stepPhases
stepPhases,
stepHistory
);

result = (await run.resumeAsync({
Expand Down
1 change: 1 addition & 0 deletions test/isolated/init-wizard-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ const mockHandleLocalOp = mock(() =>
);
mock.module("../../src/lib/init/local-ops.js", () => ({
handleLocalOp: mockHandleLocalOp,
precomputeDirListing: () => [],
validateCommand: () => {
/* noop mock */
},
Expand Down
47 changes: 47 additions & 0 deletions test/lib/init/local-ops.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import fs, { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import {
handleLocalOp,
precomputeDirListing,
validateCommand,
} from "../../../src/lib/init/local-ops.js";
import type {
Expand Down Expand Up @@ -768,3 +769,49 @@ describe("handleLocalOp", () => {
});
});
});

describe("precomputeDirListing", () => {
let testDir: string;

beforeEach(() => {
testDir = mkdtempSync(join("/tmp", "precompute-test-"));
});

afterEach(() => {
rmSync(testDir, { recursive: true, force: true });
});

test("returns DirEntry[] directly", () => {
writeFileSync(join(testDir, "app.ts"), "x");
mkdirSync(join(testDir, "src"));

const entries = precomputeDirListing(testDir);

expect(Array.isArray(entries)).toBe(true);
expect(entries.length).toBeGreaterThanOrEqual(2);

const names = entries.map((e) => e.name).sort();
expect(names).toContain("app.ts");
expect(names).toContain("src");

const file = entries.find((e) => e.name === "app.ts");
expect(file?.type).toBe("file");

const dir = entries.find((e) => e.name === "src");
expect(dir?.type).toBe("directory");
});

test("returns empty array for non-existent directory", () => {
const entries = precomputeDirListing(join(testDir, "nope"));
expect(entries).toEqual([]);
});

test("recursively lists nested entries", () => {
mkdirSync(join(testDir, "a"));
writeFileSync(join(testDir, "a", "nested.ts"), "x");

const entries = precomputeDirListing(testDir);
const paths = entries.map((e) => e.path);
expect(paths).toContain(join("a", "nested.ts"));
});
});
5 changes: 5 additions & 0 deletions test/lib/init/wizard-runner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ let formatBannerSpy: ReturnType<typeof spyOn>;
let formatResultSpy: ReturnType<typeof spyOn>;
let formatErrorSpy: ReturnType<typeof spyOn>;
let handleLocalOpSpy: ReturnType<typeof spyOn>;
let precomputeDirListingSpy: ReturnType<typeof spyOn>;
let handleInteractiveSpy: ReturnType<typeof spyOn>;

// MastraClient
Expand Down Expand Up @@ -143,6 +144,9 @@ beforeEach(() => {
ok: true,
data: { results: [] },
});
precomputeDirListingSpy = spyOn(ops, "precomputeDirListing").mockReturnValue(
[]
);
handleInteractiveSpy = spyOn(inter, "handleInteractive").mockResolvedValue({
action: "continue",
});
Expand All @@ -169,6 +173,7 @@ afterEach(() => {
formatResultSpy.mockRestore();
formatErrorSpy.mockRestore();
handleLocalOpSpy.mockRestore();
precomputeDirListingSpy.mockRestore();
handleInteractiveSpy.mockRestore();

stderrSpy.mockRestore();
Expand Down