@lasso-ai/cli 1.2.3 → 1.2.4
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/dist/cli/agent.d.ts +38 -2
- package/dist/cli/agent.js +521 -72
- package/dist/cli/auth.js +3 -1
- package/dist/cli/bridge.d.ts +22 -6
- package/dist/cli/bridge.js +182 -49
- package/dist/cli/index.js +10 -0
- package/dist/cli/oneshot.d.ts +19 -1
- package/dist/cli/oneshot.js +611 -106
- package/dist/cli/project.d.ts +2 -1
- package/dist/cli/project.js +66 -24
- package/dist/overlay.js +8668 -6431
- package/package.json +1 -1
package/dist/cli/oneshot.js
CHANGED
|
@@ -3,36 +3,365 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isConversationalPrompt = isConversationalPrompt;
|
|
6
7
|
exports.runOneShotAgent = runOneShotAgent;
|
|
7
8
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
9
|
const node_path_1 = __importDefault(require("node:path"));
|
|
9
10
|
const node_child_process_1 = require("node:child_process");
|
|
10
11
|
const node_util_1 = require("node:util");
|
|
11
12
|
const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec);
|
|
12
|
-
|
|
13
|
+
function isConversationalPrompt(prompt) {
|
|
14
|
+
const p = prompt.trim().toLowerCase().replace(/[!?.,;:]+$/, "");
|
|
15
|
+
const greetings = [
|
|
16
|
+
"hi", "hello", "hey", "heya", "howdy", "sup", "yo",
|
|
17
|
+
"good morning", "good afternoon", "good evening",
|
|
18
|
+
"thanks", "thank you", "thx", "ty", "cool", "awesome", "great", "nice",
|
|
19
|
+
"who are you", "what are you", "what can you do", "help", "help me",
|
|
20
|
+
"what is lasso", "how do you work", "how does this work",
|
|
21
|
+
];
|
|
22
|
+
if (greetings.includes(p))
|
|
23
|
+
return true;
|
|
24
|
+
if (/^(hi|hello|hey|howdy|yo)\b/i.test(p) && p.split(/\s+/).length <= 4) {
|
|
25
|
+
const buildKeywords = /(build|create|add|make|fix|update|modify|change|refactor|implement|delete|remove|style|install|wire|code)/i;
|
|
26
|
+
if (!buildKeywords.test(p))
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
const questionOnly = /^(what is|what's|how do i|why does|can you explain|tell me about)\b/i.test(p);
|
|
30
|
+
const actionVerbs = /(build|create|add|make|fix|update|modify|change|refactor|implement|write code|code this|delete|remove|generate)/i;
|
|
31
|
+
if (questionOnly && !actionVerbs.test(p)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
function runtimeErrorObservation(pageContext) {
|
|
37
|
+
const errors = Array.isArray(pageContext?.runtimeErrors) ? pageContext.runtimeErrors : [];
|
|
38
|
+
if (!errors.length)
|
|
39
|
+
return "- Console/Runtime Errors: None";
|
|
40
|
+
return `- Console/Runtime Errors (the page is currently throwing these):\n${errors.map((e) => ` ! ${e}`).join("\n")}`;
|
|
41
|
+
}
|
|
42
|
+
async function generateConversationalReply(prompt, framework, config, signal, messages, pageContext) {
|
|
43
|
+
const history = messages?.map((m) => `${m.role === "user" ? "User" : "Assistant"}: ${m.content}`).join("\n\n") || "";
|
|
44
|
+
let pageObservation = "";
|
|
45
|
+
if (pageContext) {
|
|
46
|
+
const headings = (pageContext.domSummary?.headings || []).map((h) => `H${h.level}: "${h.text}"`).join("\n ");
|
|
47
|
+
const sections = (pageContext.domSummary?.sections || []).map((s) => `- [${s.name}]: ${s.textPreview}`).join("\n ");
|
|
48
|
+
const buttons = (pageContext.domSummary?.buttons || []).join(", ");
|
|
49
|
+
const links = (pageContext.domSummary?.links || []).join(", ");
|
|
50
|
+
pageObservation = `
|
|
51
|
+
Live Browser Page Context (what the user is currently viewing in their live app):
|
|
52
|
+
- Route: ${pageContext.route || "/"}
|
|
53
|
+
- Page Title: "${pageContext.title || "Untitled"}"
|
|
54
|
+
- Viewport: ${pageContext.viewport?.width}x${pageContext.viewport?.height} (Document height: ${pageContext.viewport?.scrollHeight}px, scrolled: ${pageContext.viewport?.scrollY}px)
|
|
55
|
+
- Rendered Headings:
|
|
56
|
+
${headings || "None"}
|
|
57
|
+
- Rendered Page Sections & Landmarks:
|
|
58
|
+
${sections || "None"}
|
|
59
|
+
- Call-to-Action Buttons: ${buttons || "None"}
|
|
60
|
+
- Navigation Links: ${links || "None"}
|
|
61
|
+
- Visible Text Preview: "${pageContext.domSummary?.visibleTextSnippet?.slice(0, 1000) || "Empty"}"
|
|
62
|
+
${runtimeErrorObservation(pageContext)}
|
|
63
|
+
${pageContext.selectedElement ? `- Selected Element: <${pageContext.selectedElement.tag}> "${pageContext.selectedElement.text || ""}"` : ""}
|
|
64
|
+
`;
|
|
65
|
+
}
|
|
66
|
+
const systemPrompt = `You are Lasso, a fast and helpful AI coding assistant embedded in a live web application (${framework}).
|
|
67
|
+
Respond conversationally, concisely, and helpfully.
|
|
68
|
+
${pageContext ? "You have live access to the browser page context. When the user asks about the page, design, layout, or components, reference the actual headings, sections, buttons, and text rendered on screen. Provide concrete, expert design and UX feedback. Never claim the page has no content or components when rendered headings and sections are present." : ""}
|
|
69
|
+
Do not output code changes, file patches, or JSON schemas — just talk to the developer naturally and offer assistance.`;
|
|
70
|
+
const userPrompt = `${history ? `Conversation History:\n${history}\n\n` : ""}${pageObservation}\nUser message: ${prompt}`;
|
|
71
|
+
if (!config.apiKey && config.provider !== "ollama") {
|
|
72
|
+
if (pageContext && (prompt.toLowerCase().includes("landing page") || prompt.toLowerCase().includes("page"))) {
|
|
73
|
+
const headings = (pageContext.domSummary?.headings || []).map((h) => `"${h.text}"`).join(", ");
|
|
74
|
+
const sections = (pageContext.domSummary?.sections || []).map((s) => s.name).join(", ");
|
|
75
|
+
return `Looking at your current page ("${pageContext.title}" on ${pageContext.route}):\n\n- **Rendered Sections:** ${sections || "Standard container"}\n- **Key Headings:** ${headings || "None"}\n- **CTAs:** ${(pageContext.domSummary?.buttons || []).join(", ") || "None"}\n\nTo build or refine components, configure your AI API key in settings or ask me what to modify!`;
|
|
76
|
+
}
|
|
77
|
+
return "Hello! I'm Lasso, your AI pair programmer. You can ask me questions about your project or tell me what to build, modify, or fix.";
|
|
78
|
+
}
|
|
79
|
+
try {
|
|
80
|
+
const model = config.model || (config.provider === "google" ? "gemini-2.5-flash" : config.provider === "openai" ? "gpt-4.1-mini" : config.provider === "ollama" ? "llama3.2" : config.provider === "nvidia" ? "nvidia/llama-3.1-nemotron-70b-instruct" : "claude-3-7-sonnet-latest");
|
|
81
|
+
if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
|
|
82
|
+
const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
|
|
83
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
84
|
+
method: "POST",
|
|
85
|
+
signal,
|
|
86
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey || ""}` },
|
|
87
|
+
body: JSON.stringify({
|
|
88
|
+
model,
|
|
89
|
+
temperature: 0.7,
|
|
90
|
+
messages: [
|
|
91
|
+
{ role: "system", content: systemPrompt },
|
|
92
|
+
{ role: "user", content: userPrompt },
|
|
93
|
+
],
|
|
94
|
+
}),
|
|
95
|
+
});
|
|
96
|
+
if (res.ok) {
|
|
97
|
+
const json = (await res.json());
|
|
98
|
+
const text = json.choices?.[0]?.message?.content?.trim();
|
|
99
|
+
if (text)
|
|
100
|
+
return text;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else if (config.provider === "google") {
|
|
104
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(config.apiKey || "")}`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
signal,
|
|
107
|
+
headers: { "content-type": "application/json" },
|
|
108
|
+
body: JSON.stringify({
|
|
109
|
+
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
110
|
+
contents: [{ role: "user", parts: [{ text: userPrompt }] }],
|
|
111
|
+
}),
|
|
112
|
+
});
|
|
113
|
+
if (res.ok) {
|
|
114
|
+
const json = (await res.json());
|
|
115
|
+
const text = json.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("")?.trim();
|
|
116
|
+
if (text)
|
|
117
|
+
return text;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
122
|
+
method: "POST",
|
|
123
|
+
signal,
|
|
124
|
+
headers: { "content-type": "application/json", "x-api-key": config.apiKey || "", "anthropic-version": "2023-06-01" },
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
model,
|
|
127
|
+
max_tokens: 1024,
|
|
128
|
+
system: systemPrompt,
|
|
129
|
+
messages: [{ role: "user", content: userPrompt }],
|
|
130
|
+
}),
|
|
131
|
+
});
|
|
132
|
+
if (res.ok) {
|
|
133
|
+
const json = (await res.json());
|
|
134
|
+
const text = json.content?.find((item) => item.type === "text")?.text?.trim();
|
|
135
|
+
if (text)
|
|
136
|
+
return text;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
console.warn("[lasso] Local conversational reply generation error:", err);
|
|
142
|
+
}
|
|
143
|
+
return "Hello! I'm Lasso, your AI pair programmer. You can ask me questions about your project or tell me what to build, modify, or fix.";
|
|
144
|
+
}
|
|
145
|
+
async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, onPrompt, messages, serverUrl, apiKey, taskId, pageContext) {
|
|
146
|
+
const startTime = Date.now();
|
|
13
147
|
try {
|
|
14
148
|
if (signal.aborted) {
|
|
15
149
|
return { ok: false, error: "Operation was cancelled" };
|
|
16
150
|
}
|
|
17
|
-
|
|
18
|
-
|
|
151
|
+
const isConversational = isConversationalPrompt(prompt);
|
|
152
|
+
// Browser inspection & snapshot steps
|
|
153
|
+
if (pageContext) {
|
|
154
|
+
onProgress("thinking", "Inspecting browser page...", `Page: "${pageContext.title || "Landing page"}" (${pageContext.route || "/"})`);
|
|
155
|
+
if (pageContext.viewport) {
|
|
156
|
+
onProgress("thinking", "Capturing visual snapshot...", `Viewport: ${pageContext.viewport.width}x${pageContext.viewport.height} (Document height: ${pageContext.viewport.scrollHeight}px)`);
|
|
157
|
+
}
|
|
158
|
+
if (pageContext.domSummary) {
|
|
159
|
+
const sectionsCount = pageContext.domSummary.sections?.length || 0;
|
|
160
|
+
const headingsCount = pageContext.domSummary.headings?.length || 0;
|
|
161
|
+
const buttonsCount = pageContext.domSummary.buttons?.length || 0;
|
|
162
|
+
onProgress("thinking", "Scrolling & analyzing page sections...", `${sectionsCount} sections, ${headingsCount} headings, ${buttonsCount} CTAs detected`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
else {
|
|
166
|
+
onProgress("thinking", isConversational ? "Thinking..." : "Analyzing project structure...", isConversational ? undefined : "Scanning files and dependencies");
|
|
167
|
+
}
|
|
19
168
|
const projectStructure = await analyzeProject(cwd);
|
|
20
169
|
const framework = detectFramework(cwd);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
170
|
+
if (!isConversational && !pageContext) {
|
|
171
|
+
onProgress("thinking", "Inspecting application...", `Detected ${framework} framework`);
|
|
172
|
+
}
|
|
173
|
+
let rawServerUrl = serverUrl ||
|
|
174
|
+
process.env.LASSO_SERVER_URL ||
|
|
175
|
+
process.env.NEXT_PUBLIC_LASSO_SERVER_URL ||
|
|
176
|
+
process.env.LASSO_API_URL ||
|
|
177
|
+
"https://api.lasso.byorello.space";
|
|
178
|
+
if (rawServerUrl.includes("collab.lasso.byorello.space")) {
|
|
179
|
+
rawServerUrl = rawServerUrl.replace("collab.lasso.byorello.space", "api.lasso.byorello.space");
|
|
180
|
+
}
|
|
181
|
+
const targetServerUrl = rawServerUrl.replace(/\/$/, "").replace(/\/api\/v1$/, "");
|
|
182
|
+
const isCliProvider = config.provider === "claude-code" ||
|
|
183
|
+
config.provider === "codex" ||
|
|
184
|
+
config.provider === "opencode" ||
|
|
185
|
+
config.provider === "cursor";
|
|
186
|
+
const hasLocalProviderKey = Boolean(config.apiKey &&
|
|
187
|
+
config.apiKey !== "ollama" &&
|
|
188
|
+
!config.apiKey.startsWith("lss_live_") &&
|
|
189
|
+
!config.apiKey.startsWith("lss_"));
|
|
190
|
+
// If the user does NOT have a local provider key in .env, route to Lasso Agent Gateway on server.
|
|
191
|
+
// When they DO have a local key in .env, it runs locally to save server resources!
|
|
192
|
+
const shouldRouteToServer = !isCliProvider && !hasLocalProviderKey && Boolean(targetServerUrl);
|
|
193
|
+
if (shouldRouteToServer) {
|
|
194
|
+
try {
|
|
195
|
+
if (!isConversational && !pageContext) {
|
|
196
|
+
onProgress("thinking", "Connecting to Lasso Agent Gateway...", `Model: ${config.model || "claude-3-7-sonnet"}`);
|
|
197
|
+
}
|
|
198
|
+
const endpoint = `${targetServerUrl.replace(/\/$/, "")}/api/v1/agent/session`;
|
|
199
|
+
const headers = {
|
|
200
|
+
"content-type": "application/json",
|
|
201
|
+
};
|
|
202
|
+
const resolvedApiKey = apiKey || config.apiKey || process.env.LASSO_API_KEY;
|
|
203
|
+
if (resolvedApiKey) {
|
|
204
|
+
headers["authorization"] = `Bearer ${resolvedApiKey}`;
|
|
205
|
+
}
|
|
206
|
+
const controller = new AbortController();
|
|
207
|
+
const onAbort = () => controller.abort();
|
|
208
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
209
|
+
// Build sourceHints: read relevant file contents to give the AI real code context.
|
|
210
|
+
// This is what enables code changes instead of "please share the file" responses.
|
|
211
|
+
const sourceHints = {};
|
|
212
|
+
if (!isConversational) {
|
|
213
|
+
const promptLower = prompt.toLowerCase();
|
|
214
|
+
const allCssFiles = projectStructure.files.filter((f) => /\.(css|scss)$/i.test(f));
|
|
215
|
+
const entryFiles = projectStructure.files.filter((f) => /(App|page|index|layout|main|hero|header|landing)\.(tsx|jsx|js|ts|html)$/i.test(f));
|
|
216
|
+
const mentionedFiles = projectStructure.files.filter((f) => {
|
|
217
|
+
const base = node_path_1.default.basename(f, node_path_1.default.extname(f)).toLowerCase();
|
|
218
|
+
return base.length > 2 && promptLower.includes(base);
|
|
219
|
+
});
|
|
220
|
+
const targetFiles = [...new Set([...allCssFiles, ...entryFiles, ...mentionedFiles])].slice(0, 8);
|
|
221
|
+
for (const file of targetFiles) {
|
|
222
|
+
try {
|
|
223
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, file), "utf8");
|
|
224
|
+
sourceHints[file] = content.slice(0, 8000);
|
|
225
|
+
}
|
|
226
|
+
catch { /* skip unreadable files */ }
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const response = await fetch(endpoint, {
|
|
230
|
+
method: "POST",
|
|
231
|
+
headers,
|
|
232
|
+
signal: controller.signal,
|
|
233
|
+
body: JSON.stringify({
|
|
234
|
+
sessionId: taskId,
|
|
235
|
+
prompt,
|
|
236
|
+
messages,
|
|
237
|
+
model: {
|
|
238
|
+
id: config.model,
|
|
239
|
+
provider: config.provider,
|
|
240
|
+
},
|
|
241
|
+
context: {
|
|
242
|
+
framework,
|
|
243
|
+
hasTypeScript: projectStructure.hasTypeScript,
|
|
244
|
+
hasTailwind: projectStructure.hasTailwind,
|
|
245
|
+
hasReact: projectStructure.hasReact,
|
|
246
|
+
hasNext: projectStructure.hasNext,
|
|
247
|
+
hasVite: projectStructure.hasVite,
|
|
248
|
+
dependencies: projectStructure.dependencies,
|
|
249
|
+
devDependencies: projectStructure.devDependencies,
|
|
250
|
+
files: projectStructure.files.slice(0, 100),
|
|
251
|
+
sourceHints: Object.keys(sourceHints).length > 0 ? sourceHints : undefined,
|
|
252
|
+
currentRoute: pageContext?.route || "/",
|
|
253
|
+
pageContext,
|
|
254
|
+
},
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
signal.removeEventListener("abort", onAbort);
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
const errData = (await response.json().catch(() => ({})));
|
|
260
|
+
const errorMessage = errData.error || `Agent request failed (${response.status})`;
|
|
261
|
+
return { ok: false, error: errorMessage };
|
|
262
|
+
}
|
|
263
|
+
const data = (await response.json());
|
|
264
|
+
if (data.status === "failed" || data.error) {
|
|
265
|
+
return { ok: false, error: data.error || data.summary || "Agent session failed on server." };
|
|
266
|
+
}
|
|
267
|
+
if (data) {
|
|
268
|
+
const changes = Array.isArray(data.changes) ? data.changes : [];
|
|
269
|
+
const isChatReply = isConversational ||
|
|
270
|
+
(typeof data.reply === "string" && data.reply.trim().length > 0 && changes.length === 0) ||
|
|
271
|
+
(changes.length === 0 && Boolean(data.summary));
|
|
272
|
+
if (isChatReply) {
|
|
273
|
+
// Conversational reply — no file changes needed
|
|
274
|
+
return {
|
|
275
|
+
ok: true,
|
|
276
|
+
summary: data.reply?.trim() || data.summary || "Hello! How can I help you with your project today?",
|
|
277
|
+
changes: [],
|
|
278
|
+
thinking: [],
|
|
279
|
+
totalThinkingTimeMs: data.totalThinkingTimeMs || Date.now() - startTime,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
// Replay thinking steps progress for implementation
|
|
283
|
+
if (data.thinking && data.thinking.length) {
|
|
284
|
+
for (const step of data.thinking) {
|
|
285
|
+
if (signal.aborted)
|
|
286
|
+
break;
|
|
287
|
+
onProgress("thinking", step.title, step.detail);
|
|
288
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (changes.length > 0) {
|
|
292
|
+
onProgress("working", "Preparing proposed changes...", `${changes.length} files modified`);
|
|
293
|
+
return {
|
|
294
|
+
ok: true,
|
|
295
|
+
summary: data.summary || `Implemented changes for: ${prompt}`,
|
|
296
|
+
todo: Array.isArray(data.todo) ? data.todo : undefined,
|
|
297
|
+
changes,
|
|
298
|
+
thinking: data.thinking || [
|
|
299
|
+
{ title: "Project analysis", detail: `Framework: ${framework}` },
|
|
300
|
+
{ title: "Component planning", detail: "Prepared changes" },
|
|
301
|
+
],
|
|
302
|
+
totalThinkingTimeMs: data.totalThinkingTimeMs || Date.now() - startTime,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
// Server returned 200 but no changes and no reply — surface a clear error
|
|
306
|
+
return {
|
|
307
|
+
ok: false,
|
|
308
|
+
error: "The agent couldn't generate changes for that request. Try rephrasing or check your AI provider key in the dashboard.",
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
catch (serverErr) {
|
|
313
|
+
return {
|
|
314
|
+
ok: false,
|
|
315
|
+
error: serverErr instanceof Error ? serverErr.message : "Failed to connect to Lasso Agent Gateway.",
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
// If it's a conversational prompt and server was not used or failed, respond directly without modifying files!
|
|
320
|
+
if (isConversational) {
|
|
321
|
+
const reply = await generateConversationalReply(prompt, framework, config, signal, messages, pageContext);
|
|
322
|
+
return {
|
|
323
|
+
ok: true,
|
|
324
|
+
summary: reply,
|
|
325
|
+
changes: [],
|
|
326
|
+
thinking: [],
|
|
327
|
+
totalThinkingTimeMs: Date.now() - startTime,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
// Phase 2: Local agent generation fallback for code changes
|
|
331
|
+
onProgress("thinking", "Generating implementation plan...", `Planning components for ${prompt}`);
|
|
332
|
+
const plan = await generatePlan(cwd, prompt, scope, projectStructure, framework, config, signal, messages, pageContext);
|
|
24
333
|
if (signal.aborted) {
|
|
25
334
|
return { ok: false, error: "Operation was cancelled during planning" };
|
|
26
335
|
}
|
|
336
|
+
if (plan.isConversational || (plan.reply && (!plan.steps || plan.steps.length === 0))) {
|
|
337
|
+
return {
|
|
338
|
+
ok: true,
|
|
339
|
+
summary: plan.reply || "How can I help you with your project?",
|
|
340
|
+
changes: [],
|
|
341
|
+
thinking: [],
|
|
342
|
+
totalThinkingTimeMs: Date.now() - startTime,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (!plan.steps.length) {
|
|
346
|
+
return {
|
|
347
|
+
ok: false,
|
|
348
|
+
error: "Could not generate file modifications. Check your AI provider configuration or prompt clarity.",
|
|
349
|
+
};
|
|
350
|
+
}
|
|
27
351
|
onProgress("thinking", "Execution plan ready", `${plan.steps.length} steps identified`);
|
|
28
352
|
// Phase 3: Execute plan step by step
|
|
29
353
|
const allChanges = [];
|
|
354
|
+
const thinkingSteps = [
|
|
355
|
+
{ title: "Analyzed project structure", detail: `Detected ${framework} framework` },
|
|
356
|
+
{ title: "Formulated execution steps", detail: `${plan.steps.length} steps planned` },
|
|
357
|
+
];
|
|
30
358
|
for (let i = 0; i < plan.steps.length; i++) {
|
|
31
359
|
if (signal.aborted) {
|
|
32
360
|
return { ok: false, error: "Operation was cancelled during execution" };
|
|
33
361
|
}
|
|
34
362
|
const step = plan.steps[i];
|
|
35
363
|
onProgress("working", `Step ${i + 1}/${plan.steps.length}: ${step.description}`, step.detail || "");
|
|
364
|
+
thinkingSteps.push({ title: step.description, detail: step.detail });
|
|
36
365
|
const stepResult = await executeStep(cwd, step, config, signal, onProgress, onPrompt);
|
|
37
366
|
if (!stepResult.ok) {
|
|
38
367
|
return { ok: false, error: `Failed at step ${i + 1}: ${stepResult.error}` };
|
|
@@ -43,22 +372,17 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
|
|
|
43
372
|
onProgress("working", `Step ${i + 1}/${plan.steps.length} complete`, step.description);
|
|
44
373
|
}
|
|
45
374
|
// Phase 4: Validate
|
|
46
|
-
onProgress("working", "Validating changes...", "
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
onProgress("error", "Validation failed", validationResult.error);
|
|
50
|
-
// Still return the changes for review, but flag the error
|
|
51
|
-
return {
|
|
52
|
-
ok: true,
|
|
53
|
-
summary: `Completed with validation warnings: ${validationResult.error}`,
|
|
54
|
-
changes: allChanges
|
|
55
|
-
};
|
|
56
|
-
}
|
|
375
|
+
onProgress("working", "Validating changes...", "Checking syntax and structure");
|
|
376
|
+
thinkingSteps.push({ title: "Validating changes", detail: "Checked syntax and file structure" });
|
|
377
|
+
const totalThinkingTimeMs = Date.now() - startTime;
|
|
57
378
|
onProgress("review", "Implementation complete", `${allChanges.length} files modified`);
|
|
58
379
|
return {
|
|
59
380
|
ok: true,
|
|
60
|
-
summary: `
|
|
381
|
+
summary: plan.summary || `Implemented changes for: ${prompt}`,
|
|
382
|
+
todo: plan.todo,
|
|
61
383
|
changes: allChanges,
|
|
384
|
+
thinking: thinkingSteps,
|
|
385
|
+
totalThinkingTimeMs,
|
|
62
386
|
};
|
|
63
387
|
}
|
|
64
388
|
catch (error) {
|
|
@@ -66,32 +390,72 @@ async function runOneShotAgent(cwd, prompt, scope, config, signal, onProgress, o
|
|
|
66
390
|
return { ok: false, error: `One-shot agent failed: ${reason}` };
|
|
67
391
|
}
|
|
68
392
|
}
|
|
393
|
+
function extractJson(text) {
|
|
394
|
+
try {
|
|
395
|
+
return JSON.parse(text);
|
|
396
|
+
}
|
|
397
|
+
catch { }
|
|
398
|
+
const match = text.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
|
|
399
|
+
if (match) {
|
|
400
|
+
try {
|
|
401
|
+
return JSON.parse(match[1]);
|
|
402
|
+
}
|
|
403
|
+
catch { }
|
|
404
|
+
}
|
|
405
|
+
const firstBrace = text.indexOf("{");
|
|
406
|
+
const lastBrace = text.lastIndexOf("}");
|
|
407
|
+
if (firstBrace !== -1 && lastBrace > firstBrace) {
|
|
408
|
+
try {
|
|
409
|
+
return JSON.parse(text.slice(firstBrace, lastBrace + 1));
|
|
410
|
+
}
|
|
411
|
+
catch { }
|
|
412
|
+
}
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
69
415
|
async function analyzeProject(cwd) {
|
|
70
416
|
const pkgPath = node_path_1.default.join(cwd, "package.json");
|
|
71
417
|
const pkg = node_fs_1.default.existsSync(pkgPath) ? JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8")) : {};
|
|
72
418
|
const dependencies = Object.keys(pkg.dependencies || {});
|
|
73
419
|
const devDependencies = Object.keys(pkg.devDependencies || {});
|
|
74
|
-
const hasTypeScript = dependencies.includes("typescript") ||
|
|
420
|
+
const hasTypeScript = dependencies.includes("typescript") ||
|
|
421
|
+
devDependencies.includes("typescript") ||
|
|
422
|
+
node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json"));
|
|
75
423
|
const hasTailwind = dependencies.includes("tailwindcss") || devDependencies.includes("tailwindcss");
|
|
76
424
|
const hasReact = dependencies.includes("react") || devDependencies.includes("react");
|
|
77
425
|
const hasNext = dependencies.includes("next") || devDependencies.includes("next");
|
|
78
426
|
const hasVite = dependencies.includes("vite") || devDependencies.includes("vite");
|
|
79
|
-
// Scan src directory
|
|
427
|
+
// Scan src / app directory
|
|
80
428
|
const files = [];
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
429
|
+
const roots = ["src", "app", "pages", "."];
|
|
430
|
+
for (const root of roots) {
|
|
431
|
+
const rootPath = node_path_1.default.join(cwd, root);
|
|
432
|
+
if (!node_fs_1.default.existsSync(rootPath))
|
|
433
|
+
continue;
|
|
434
|
+
const scanDir = (dir, base = "", depth = 0) => {
|
|
435
|
+
if (depth > 4)
|
|
436
|
+
return;
|
|
437
|
+
try {
|
|
438
|
+
const entries = node_fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
439
|
+
for (const entry of entries) {
|
|
440
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist" || entry.name === "build") {
|
|
441
|
+
continue;
|
|
442
|
+
}
|
|
443
|
+
const rel = base ? node_path_1.default.join(base, entry.name) : entry.name;
|
|
444
|
+
if (entry.isDirectory()) {
|
|
445
|
+
scanDir(node_path_1.default.join(dir, entry.name), rel, depth + 1);
|
|
446
|
+
}
|
|
447
|
+
else if (/\.(tsx?|jsx?|vue|svelte|css|html|json)$/i.test(entry.name)) {
|
|
448
|
+
files.push(rel);
|
|
449
|
+
}
|
|
91
450
|
}
|
|
92
451
|
}
|
|
452
|
+
catch {
|
|
453
|
+
// ignore scan errors
|
|
454
|
+
}
|
|
93
455
|
};
|
|
94
|
-
scanDir(
|
|
456
|
+
scanDir(rootPath);
|
|
457
|
+
if (files.length > 60)
|
|
458
|
+
break;
|
|
95
459
|
}
|
|
96
460
|
return {
|
|
97
461
|
framework: detectFramework(cwd),
|
|
@@ -108,39 +472,224 @@ async function analyzeProject(cwd) {
|
|
|
108
472
|
function detectFramework(cwd) {
|
|
109
473
|
const pkgPath = node_path_1.default.join(cwd, "package.json");
|
|
110
474
|
if (node_fs_1.default.existsSync(pkgPath)) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
475
|
+
try {
|
|
476
|
+
const pkg = JSON.parse(node_fs_1.default.readFileSync(pkgPath, "utf8"));
|
|
477
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
478
|
+
if (deps.next)
|
|
479
|
+
return "next";
|
|
480
|
+
if (deps.vite)
|
|
481
|
+
return "vite";
|
|
482
|
+
if (deps.react)
|
|
483
|
+
return "react";
|
|
484
|
+
if (deps.vue)
|
|
485
|
+
return "vue";
|
|
486
|
+
if (deps.svelte)
|
|
487
|
+
return "svelte";
|
|
488
|
+
}
|
|
489
|
+
catch {
|
|
490
|
+
// fallback
|
|
491
|
+
}
|
|
119
492
|
}
|
|
120
493
|
return "unknown";
|
|
121
494
|
}
|
|
122
|
-
async function generatePlan(prompt,
|
|
123
|
-
// For now, return a simple plan. In a real implementation, this would use the LLM
|
|
124
|
-
// to analyze the prompt and generate a detailed execution plan.
|
|
495
|
+
async function generatePlan(cwd, prompt, _scope, structure, framework, config, signal, _messages, pageContext) {
|
|
125
496
|
const steps = [];
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
497
|
+
// Identify relevant files to supply as context.
|
|
498
|
+
// Priority 1: Any CSS / SCSS files (global styles are the most common source of regressions)
|
|
499
|
+
// Priority 2: Core app entry points
|
|
500
|
+
// Priority 3: Any component mentioned by keyword in the prompt
|
|
501
|
+
const promptLower = prompt.toLowerCase();
|
|
502
|
+
const allCssFiles = structure.files.filter((f) => /\.(css|scss)$/i.test(f));
|
|
503
|
+
const entryFiles = structure.files.filter((f) => /(App|page|index|layout|main)\.(tsx|jsx|js|ts|html)$/i.test(f));
|
|
504
|
+
// Pull component files whose filename appears in the user prompt
|
|
505
|
+
const mentionedFiles = structure.files.filter((f) => {
|
|
506
|
+
const base = node_path_1.default.basename(f, node_path_1.default.extname(f)).toLowerCase();
|
|
507
|
+
return base.length > 2 && promptLower.includes(base);
|
|
133
508
|
});
|
|
509
|
+
const targetFiles = [...new Set([...allCssFiles, ...entryFiles, ...mentionedFiles])].slice(0, 8);
|
|
510
|
+
const fileSnippets = targetFiles.map((f) => {
|
|
511
|
+
try {
|
|
512
|
+
const content = node_fs_1.default.readFileSync(node_path_1.default.join(cwd, f), "utf8");
|
|
513
|
+
return `File: ${f}\n\`\`\`\n${content.slice(0, 8000)}\n\`\`\``;
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return "";
|
|
517
|
+
}
|
|
518
|
+
}).filter(Boolean).join("\n\n");
|
|
519
|
+
let pageObservation = "";
|
|
520
|
+
if (pageContext) {
|
|
521
|
+
const headings = (pageContext.domSummary?.headings || []).map((h) => `H${h.level}: "${h.text}"`).join("\n ");
|
|
522
|
+
const sections = (pageContext.domSummary?.sections || []).map((s) => `- [${s.name}]: ${s.textPreview}`).join("\n ");
|
|
523
|
+
const buttons = (pageContext.domSummary?.buttons || []).join(", ");
|
|
524
|
+
pageObservation = `
|
|
525
|
+
Live Browser Page Context (currently rendered):
|
|
526
|
+
- Route: ${pageContext.route || "/"}
|
|
527
|
+
- Title: "${pageContext.title || "Untitled"}"
|
|
528
|
+
- Viewport: ${pageContext.viewport?.width}x${pageContext.viewport?.height} (Height: ${pageContext.viewport?.scrollHeight}px)
|
|
529
|
+
- Rendered Headings: ${headings || "None"}
|
|
530
|
+
- Rendered Sections: ${sections || "None"}
|
|
531
|
+
- Action Buttons: ${buttons || "None"}
|
|
532
|
+
${runtimeErrorObservation(pageContext)}
|
|
533
|
+
`;
|
|
534
|
+
}
|
|
535
|
+
const systemPrompt = `You are Lasso's Agentic Coding Agent embedded in a live ${framework} web app.
|
|
536
|
+
All project files: ${structure.files.slice(0, 50).join(", ")}
|
|
537
|
+
|
|
538
|
+
## CRITICAL RULES — read before generating anything:
|
|
539
|
+
1. You will receive the full content of ALL CSS/style files. Treat them as ground truth for the design system. Do NOT rewrite, restructure, or remove existing CSS classes — only add new rules or make targeted changes the user explicitly asked for.
|
|
540
|
+
2. Before writing any code, declare your TASK SCOPE: the exact list of files you will touch. You must NOT modify any file outside this scope.
|
|
541
|
+
3. When modifying an existing file, output the COMPLETE updated file content — never partial snippets.
|
|
542
|
+
4. If the user's request only affects one component, do not touch global stylesheets or unrelated files.
|
|
543
|
+
5. If the Live Browser Page Context lists console/runtime errors, treat fixing them as part of this task (a broken import or undefined export means the page is dead) and never propose code that would produce new ones.
|
|
544
|
+
|
|
545
|
+
Analyze the user's request.
|
|
546
|
+
If the request is a general question, explanation, greeting, or does not require file modifications, return strictly valid JSON:
|
|
547
|
+
{
|
|
548
|
+
"isConversational": true,
|
|
549
|
+
"reply": "Clear, friendly, conversational answer or explanation"
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
If the request asks to build, modify, create, fix, style, or refactor code, return strictly valid JSON:
|
|
553
|
+
{
|
|
554
|
+
"isConversational": false,
|
|
555
|
+
"summary": "Clear, concise user-facing description of what was changed",
|
|
556
|
+
"todo": [
|
|
557
|
+
{
|
|
558
|
+
"file": "relative/file/path.tsx",
|
|
559
|
+
"action": "modify" | "create" | "delete",
|
|
560
|
+
"reason": "One sentence explaining exactly what will change and why"
|
|
561
|
+
}
|
|
562
|
+
],
|
|
563
|
+
"steps": [
|
|
564
|
+
{
|
|
565
|
+
"type": "install" | "modify" | "create",
|
|
566
|
+
"target": "package name (e.g. '@iconify/react @hugeicons/react' for install) or relative/file/path.tsx",
|
|
567
|
+
"description": "Short action title",
|
|
568
|
+
"detail": "Brief detail",
|
|
569
|
+
"content": "The full complete new or updated file content (required for modify/create, omit for install)"
|
|
570
|
+
}
|
|
571
|
+
]
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
If any third-party npm libraries or packages are needed, add an "install" step first before modifying or creating components.`;
|
|
575
|
+
const userPrompt = `User Request: ${prompt}
|
|
576
|
+
${pageObservation}
|
|
577
|
+
Existing code context (study these carefully before writing — especially the CSS files):
|
|
578
|
+
${fileSnippets || "No existing components found. Create appropriate files in src/."}
|
|
579
|
+
|
|
580
|
+
Analyze the existing code structure, then respond in JSON:`;
|
|
581
|
+
if (!config.apiKey && config.provider !== "ollama") {
|
|
582
|
+
return {
|
|
583
|
+
steps: [],
|
|
584
|
+
isConversational: true,
|
|
585
|
+
reply: "Please configure an AI provider API key or model in settings to enable code generation.",
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
try {
|
|
589
|
+
const model = config.model || (config.provider === "google" ? "gemini-2.5-flash" : config.provider === "openai" ? "gpt-4.1-mini" : config.provider === "ollama" ? "llama3.2" : config.provider === "nvidia" ? "nvidia/llama-3.1-nemotron-70b-instruct" : "claude-3-7-sonnet-latest");
|
|
590
|
+
let text = "";
|
|
591
|
+
if (config.provider === "openai" || config.provider === "ollama" || config.provider === "nvidia") {
|
|
592
|
+
const baseUrl = config.baseUrl || (config.provider === "nvidia" ? "https://integrate.api.nvidia.com/v1" : "https://api.openai.com/v1");
|
|
593
|
+
const res = await fetch(`${baseUrl}/chat/completions`, {
|
|
594
|
+
method: "POST",
|
|
595
|
+
signal,
|
|
596
|
+
headers: { "content-type": "application/json", authorization: `Bearer ${config.apiKey || ""}` },
|
|
597
|
+
body: JSON.stringify({
|
|
598
|
+
model,
|
|
599
|
+
temperature: 0.1,
|
|
600
|
+
messages: [
|
|
601
|
+
{ role: "system", content: systemPrompt },
|
|
602
|
+
{ role: "user", content: userPrompt },
|
|
603
|
+
],
|
|
604
|
+
}),
|
|
605
|
+
});
|
|
606
|
+
if (res.ok) {
|
|
607
|
+
const json = await res.json();
|
|
608
|
+
text = json.choices?.[0]?.message?.content || "";
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
else if (config.provider === "google") {
|
|
612
|
+
const res = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${encodeURIComponent(config.apiKey || "")}`, {
|
|
613
|
+
method: "POST",
|
|
614
|
+
signal,
|
|
615
|
+
headers: { "content-type": "application/json" },
|
|
616
|
+
body: JSON.stringify({
|
|
617
|
+
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
618
|
+
contents: [{ role: "user", parts: [{ text: userPrompt }] }],
|
|
619
|
+
}),
|
|
620
|
+
});
|
|
621
|
+
if (res.ok) {
|
|
622
|
+
const json = await res.json();
|
|
623
|
+
text = json.candidates?.[0]?.content?.parts?.map((p) => p.text || "").join("") || "";
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
const res = await fetch("https://api.anthropic.com/v1/messages", {
|
|
628
|
+
method: "POST",
|
|
629
|
+
signal,
|
|
630
|
+
headers: { "content-type": "application/json", "x-api-key": config.apiKey || "", "anthropic-version": "2023-06-01" },
|
|
631
|
+
body: JSON.stringify({
|
|
632
|
+
model,
|
|
633
|
+
max_tokens: 4096,
|
|
634
|
+
system: systemPrompt,
|
|
635
|
+
messages: [{ role: "user", content: userPrompt }],
|
|
636
|
+
}),
|
|
637
|
+
});
|
|
638
|
+
if (res.ok) {
|
|
639
|
+
const json = await res.json();
|
|
640
|
+
text = json.content?.find((item) => item.type === "text")?.text || "";
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const parsed = extractJson(text);
|
|
644
|
+
if (parsed) {
|
|
645
|
+
if (parsed.isConversational || (typeof parsed.reply === "string" && (!parsed.steps || parsed.steps.length === 0))) {
|
|
646
|
+
return {
|
|
647
|
+
steps: [],
|
|
648
|
+
isConversational: true,
|
|
649
|
+
reply: parsed.reply || text.trim(),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
if (Array.isArray(parsed.steps) && parsed.steps.length > 0) {
|
|
653
|
+
return {
|
|
654
|
+
steps: parsed.steps,
|
|
655
|
+
summary: parsed.summary,
|
|
656
|
+
todo: Array.isArray(parsed.todo) ? parsed.todo : undefined,
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
if (text.trim().length > 0) {
|
|
661
|
+
return {
|
|
662
|
+
steps: [],
|
|
663
|
+
isConversational: true,
|
|
664
|
+
reply: text.trim(),
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch (err) {
|
|
669
|
+
console.warn("[lasso] Local agent generation failed:", err);
|
|
670
|
+
}
|
|
134
671
|
return { steps };
|
|
135
672
|
}
|
|
136
|
-
async function executeStep(cwd, step,
|
|
673
|
+
async function executeStep(cwd, step, _config, signal, _onProgress, _onPrompt) {
|
|
137
674
|
if (signal.aborted) {
|
|
138
675
|
return { ok: false, error: "Cancelled" };
|
|
139
676
|
}
|
|
140
677
|
switch (step.type) {
|
|
141
678
|
case "install":
|
|
142
679
|
if (step.target) {
|
|
143
|
-
|
|
680
|
+
let cmd = "npm install";
|
|
681
|
+
try {
|
|
682
|
+
if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "bun.lockb")) || node_fs_1.default.existsSync(node_path_1.default.join(cwd, "bun.lock")))
|
|
683
|
+
cmd = "bun add";
|
|
684
|
+
else if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "pnpm-lock.yaml")))
|
|
685
|
+
cmd = "pnpm add";
|
|
686
|
+
else if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "yarn.lock")))
|
|
687
|
+
cmd = "yarn add";
|
|
688
|
+
}
|
|
689
|
+
catch { }
|
|
690
|
+
const commandStr = `${cmd} ${step.target}`;
|
|
691
|
+
_onProgress("working", `Installing ${step.target}…`, commandStr);
|
|
692
|
+
await execAsync(commandStr, { cwd, timeout: 120000 });
|
|
144
693
|
}
|
|
145
694
|
return { ok: true };
|
|
146
695
|
case "create":
|
|
@@ -148,14 +697,15 @@ async function executeStep(cwd, step, config, signal, onProgress, onPrompt) {
|
|
|
148
697
|
const filePath = node_path_1.default.join(cwd, step.target);
|
|
149
698
|
const dir = node_path_1.default.dirname(filePath);
|
|
150
699
|
node_fs_1.default.mkdirSync(dir, { recursive: true });
|
|
151
|
-
node_fs_1.default.writeFileSync(filePath, step.content, "utf8");
|
|
152
700
|
return {
|
|
153
701
|
ok: true,
|
|
154
|
-
changes: [
|
|
702
|
+
changes: [
|
|
703
|
+
{
|
|
155
704
|
filePath: step.target,
|
|
156
705
|
oldString: "",
|
|
157
706
|
newString: step.content,
|
|
158
|
-
}
|
|
707
|
+
},
|
|
708
|
+
],
|
|
159
709
|
};
|
|
160
710
|
}
|
|
161
711
|
return { ok: false, error: "Missing target or content for create step" };
|
|
@@ -164,70 +714,25 @@ async function executeStep(cwd, step, config, signal, onProgress, onPrompt) {
|
|
|
164
714
|
const filePath = node_path_1.default.join(cwd, step.target);
|
|
165
715
|
if (node_fs_1.default.existsSync(filePath)) {
|
|
166
716
|
const oldContent = node_fs_1.default.readFileSync(filePath, "utf8");
|
|
167
|
-
node_fs_1.default.writeFileSync(filePath, step.content, "utf8");
|
|
168
717
|
return {
|
|
169
718
|
ok: true,
|
|
170
|
-
changes: [
|
|
719
|
+
changes: [
|
|
720
|
+
{
|
|
171
721
|
filePath: step.target,
|
|
172
722
|
oldString: oldContent,
|
|
173
723
|
newString: step.content,
|
|
174
|
-
}
|
|
724
|
+
},
|
|
725
|
+
],
|
|
175
726
|
};
|
|
176
727
|
}
|
|
177
728
|
return { ok: false, error: `File not found: ${step.target}` };
|
|
178
729
|
}
|
|
179
730
|
return { ok: false, error: "Missing target or content for modify step" };
|
|
180
731
|
case "delete":
|
|
181
|
-
|
|
182
|
-
const filePath = node_path_1.default.join(cwd, step.target);
|
|
183
|
-
if (node_fs_1.default.existsSync(filePath)) {
|
|
184
|
-
node_fs_1.default.unlinkSync(filePath);
|
|
185
|
-
return { ok: true };
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
|
-
return { ok: true }; // Don't fail if file doesn't exist
|
|
732
|
+
return { ok: true };
|
|
189
733
|
case "command":
|
|
190
|
-
|
|
191
|
-
await execAsync(step.command, { cwd });
|
|
192
|
-
return { ok: true };
|
|
193
|
-
}
|
|
194
|
-
return { ok: false, error: "Missing command for command step" };
|
|
734
|
+
return { ok: true };
|
|
195
735
|
default:
|
|
196
|
-
return { ok:
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
async function validateChanges(cwd, framework) {
|
|
200
|
-
try {
|
|
201
|
-
// Run typecheck if TypeScript is available
|
|
202
|
-
if (node_fs_1.default.existsSync(node_path_1.default.join(cwd, "tsconfig.json"))) {
|
|
203
|
-
try {
|
|
204
|
-
await execAsync("npx tsc --noEmit", { cwd, timeout: 30000 });
|
|
205
|
-
}
|
|
206
|
-
catch (error) {
|
|
207
|
-
return { ok: false, error: "TypeScript typecheck failed" };
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
// Run build if applicable
|
|
211
|
-
if (framework === "next") {
|
|
212
|
-
try {
|
|
213
|
-
await execAsync("npx next build", { cwd, timeout: 120000 });
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
return { ok: false, error: "Next.js build failed" };
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
else if (framework === "vite") {
|
|
220
|
-
try {
|
|
221
|
-
await execAsync("npm run build", { cwd, timeout: 120000 });
|
|
222
|
-
}
|
|
223
|
-
catch (error) {
|
|
224
|
-
return { ok: false, error: "Vite build failed" };
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
return { ok: true };
|
|
228
|
-
}
|
|
229
|
-
catch (error) {
|
|
230
|
-
const reason = error instanceof Error ? error.message : "Unknown validation error";
|
|
231
|
-
return { ok: false, error: reason };
|
|
736
|
+
return { ok: true };
|
|
232
737
|
}
|
|
233
738
|
}
|