wdyt 0.1.0
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/LICENSE +21 -0
- package/README.md +83 -0
- package/package.json +52 -0
- package/src/cli.ts +267 -0
- package/src/commands/builder.ts +85 -0
- package/src/commands/chat.ts +306 -0
- package/src/commands/init.ts +222 -0
- package/src/commands/prompt.ts +188 -0
- package/src/commands/select.ts +180 -0
- package/src/commands/windows.ts +54 -0
- package/src/state.test.ts +184 -0
- package/src/state.ts +282 -0
- package/src/types.ts +69 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat commands - chat_send
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* - call chat_send {json}: Export context (prompt + files) to XML
|
|
6
|
+
*
|
|
7
|
+
* Compatible with flowctl.py:
|
|
8
|
+
* - Line 3975: call chat_send {payload}
|
|
9
|
+
* - Line 272-289: build_chat_payload structure
|
|
10
|
+
*
|
|
11
|
+
* Payload structure:
|
|
12
|
+
* {
|
|
13
|
+
* "message": string, // The prompt/message
|
|
14
|
+
* "mode": string, // Mode (e.g., "review")
|
|
15
|
+
* "new_chat"?: boolean, // Start new chat
|
|
16
|
+
* "chat_name"?: string, // Optional name
|
|
17
|
+
* "selected_paths"?: string[] // Files to include
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { existsSync, readFileSync, mkdirSync } from "fs";
|
|
22
|
+
import { join, dirname, basename } from "path";
|
|
23
|
+
import { homedir } from "os";
|
|
24
|
+
import { getTab, getWindow } from "../state";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Chat send payload structure (from flowctl.py build_chat_payload)
|
|
28
|
+
*/
|
|
29
|
+
export interface ChatSendPayload {
|
|
30
|
+
message: string;
|
|
31
|
+
mode: string;
|
|
32
|
+
new_chat?: boolean;
|
|
33
|
+
chat_name?: string;
|
|
34
|
+
selected_paths?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Chat send response
|
|
39
|
+
*/
|
|
40
|
+
export interface ChatSendResponse {
|
|
41
|
+
id: string;
|
|
42
|
+
path: string;
|
|
43
|
+
review?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Get the chats directory path
|
|
48
|
+
*/
|
|
49
|
+
function getChatsDir(): string {
|
|
50
|
+
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
51
|
+
if (xdgDataHome) {
|
|
52
|
+
return join(xdgDataHome, "wdyt", "chats");
|
|
53
|
+
}
|
|
54
|
+
return join(homedir(), ".wdyt", "chats");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Escape XML special characters
|
|
59
|
+
*/
|
|
60
|
+
function escapeXml(str: string): string {
|
|
61
|
+
return str
|
|
62
|
+
.replace(/&/g, "&")
|
|
63
|
+
.replace(/</g, "<")
|
|
64
|
+
.replace(/>/g, ">")
|
|
65
|
+
.replace(/"/g, """)
|
|
66
|
+
.replace(/'/g, "'");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generate a UUID v4
|
|
71
|
+
*/
|
|
72
|
+
function generateUUID(): string {
|
|
73
|
+
return crypto.randomUUID();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Read file content safely
|
|
78
|
+
*/
|
|
79
|
+
function readFileSafe(path: string): { success: boolean; content?: string; error?: string } {
|
|
80
|
+
try {
|
|
81
|
+
if (!existsSync(path)) {
|
|
82
|
+
return { success: false, error: `File not found: ${path}` };
|
|
83
|
+
}
|
|
84
|
+
const content = readFileSync(path, "utf-8");
|
|
85
|
+
return { success: true, content };
|
|
86
|
+
} catch (error) {
|
|
87
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
88
|
+
return { success: false, error: message };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Build XML content with prompt and files
|
|
94
|
+
*
|
|
95
|
+
* Format:
|
|
96
|
+
* <context>
|
|
97
|
+
* <prompt>...</prompt>
|
|
98
|
+
* <files>
|
|
99
|
+
* <file path="...">content</file>
|
|
100
|
+
* ...
|
|
101
|
+
* </files>
|
|
102
|
+
* <directory_structure>...</directory_structure>
|
|
103
|
+
* </context>
|
|
104
|
+
*/
|
|
105
|
+
function buildContextXml(
|
|
106
|
+
prompt: string,
|
|
107
|
+
files: Array<{ path: string; content: string }>,
|
|
108
|
+
directoryStructure: string
|
|
109
|
+
): string {
|
|
110
|
+
const lines: string[] = [];
|
|
111
|
+
lines.push('<?xml version="1.0" encoding="UTF-8"?>');
|
|
112
|
+
lines.push("<context>");
|
|
113
|
+
|
|
114
|
+
// Add prompt
|
|
115
|
+
lines.push(" <prompt>");
|
|
116
|
+
lines.push(` ${escapeXml(prompt)}`);
|
|
117
|
+
lines.push(" </prompt>");
|
|
118
|
+
|
|
119
|
+
// Add files
|
|
120
|
+
if (files.length > 0) {
|
|
121
|
+
lines.push(" <files>");
|
|
122
|
+
for (const file of files) {
|
|
123
|
+
lines.push(` <file path="${escapeXml(file.path)}">`);
|
|
124
|
+
lines.push(escapeXml(file.content));
|
|
125
|
+
lines.push(" </file>");
|
|
126
|
+
}
|
|
127
|
+
lines.push(" </files>");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Add directory structure
|
|
131
|
+
if (directoryStructure) {
|
|
132
|
+
lines.push(" <directory_structure>");
|
|
133
|
+
lines.push(` ${escapeXml(directoryStructure)}`);
|
|
134
|
+
lines.push(" </directory_structure>");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
lines.push("</context>");
|
|
138
|
+
return lines.join("\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build directory structure string from file paths
|
|
143
|
+
*/
|
|
144
|
+
function buildDirectoryStructure(paths: string[]): string {
|
|
145
|
+
if (paths.length === 0) return "";
|
|
146
|
+
|
|
147
|
+
// Group files by directory
|
|
148
|
+
const dirs = new Map<string, string[]>();
|
|
149
|
+
|
|
150
|
+
for (const path of paths) {
|
|
151
|
+
const dir = dirname(path);
|
|
152
|
+
const file = basename(path);
|
|
153
|
+
if (!dirs.has(dir)) {
|
|
154
|
+
dirs.set(dir, []);
|
|
155
|
+
}
|
|
156
|
+
dirs.get(dir)!.push(file);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Build tree-like structure
|
|
160
|
+
const lines: string[] = [];
|
|
161
|
+
const sortedDirs = Array.from(dirs.keys()).sort();
|
|
162
|
+
|
|
163
|
+
for (const dir of sortedDirs) {
|
|
164
|
+
lines.push(`${dir}/`);
|
|
165
|
+
const files = dirs.get(dir)!.sort();
|
|
166
|
+
for (const file of files) {
|
|
167
|
+
lines.push(` ${file}`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return lines.join("\n");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Chat send command
|
|
176
|
+
*
|
|
177
|
+
* Exports context (prompt + selected files) to an XML file
|
|
178
|
+
*
|
|
179
|
+
* @param windowId - Window ID
|
|
180
|
+
* @param tabId - Tab ID
|
|
181
|
+
* @param payloadJson - JSON string with chat_send payload
|
|
182
|
+
* @returns Chat ID in format "Chat: `<uuid>`"
|
|
183
|
+
*/
|
|
184
|
+
export async function chatSendCommand(
|
|
185
|
+
windowId: number,
|
|
186
|
+
tabId: string,
|
|
187
|
+
payloadJson: string
|
|
188
|
+
): Promise<{
|
|
189
|
+
success: boolean;
|
|
190
|
+
data?: ChatSendResponse;
|
|
191
|
+
output?: string;
|
|
192
|
+
error?: string;
|
|
193
|
+
}> {
|
|
194
|
+
try {
|
|
195
|
+
// Parse the JSON payload
|
|
196
|
+
const payload = JSON.parse(payloadJson) as ChatSendPayload;
|
|
197
|
+
|
|
198
|
+
// Get tab state for prompt and selected files
|
|
199
|
+
const tab = await getTab(windowId, tabId);
|
|
200
|
+
const window = await getWindow(windowId);
|
|
201
|
+
|
|
202
|
+
// Use message from payload as the prompt, or fall back to tab's prompt
|
|
203
|
+
const prompt = payload.message || tab.prompt;
|
|
204
|
+
|
|
205
|
+
// Determine which files to include
|
|
206
|
+
// Use selected_paths from payload if provided, otherwise use tab's selectedFiles
|
|
207
|
+
let filePaths = payload.selected_paths || tab.selectedFiles;
|
|
208
|
+
|
|
209
|
+
// Resolve relative paths against window root
|
|
210
|
+
const rootPath = window.rootFolderPaths[0] || process.cwd();
|
|
211
|
+
filePaths = filePaths.map((p) => {
|
|
212
|
+
if (p.startsWith("/")) return p;
|
|
213
|
+
return join(rootPath, p);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// Read file contents
|
|
217
|
+
const files: Array<{ path: string; content: string }> = [];
|
|
218
|
+
const errors: string[] = [];
|
|
219
|
+
|
|
220
|
+
for (const filePath of filePaths) {
|
|
221
|
+
const result = readFileSafe(filePath);
|
|
222
|
+
if (result.success && result.content !== undefined) {
|
|
223
|
+
files.push({ path: filePath, content: result.content });
|
|
224
|
+
} else {
|
|
225
|
+
// Handle missing files gracefully - just skip them and log
|
|
226
|
+
errors.push(`Skipped: ${filePath} (${result.error})`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Log any skipped files to stderr so users are aware
|
|
231
|
+
if (errors.length > 0) {
|
|
232
|
+
console.error(`Warning: ${errors.length} file(s) skipped:`);
|
|
233
|
+
for (const err of errors) {
|
|
234
|
+
console.error(` ${err}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Build directory structure from the files we successfully read
|
|
239
|
+
const directoryStructure = buildDirectoryStructure(files.map((f) => f.path));
|
|
240
|
+
|
|
241
|
+
// Build the XML content
|
|
242
|
+
const xmlContent = buildContextXml(prompt, files, directoryStructure);
|
|
243
|
+
|
|
244
|
+
// Generate chat ID
|
|
245
|
+
const chatId = generateUUID();
|
|
246
|
+
|
|
247
|
+
// Ensure chats directory exists
|
|
248
|
+
const chatsDir = getChatsDir();
|
|
249
|
+
mkdirSync(chatsDir, { recursive: true });
|
|
250
|
+
|
|
251
|
+
// Write the XML file
|
|
252
|
+
const chatPath = join(chatsDir, `${chatId}.xml`);
|
|
253
|
+
await Bun.write(chatPath, xmlContent);
|
|
254
|
+
|
|
255
|
+
// Check if this is a review request - if so, output subagent spawn instruction
|
|
256
|
+
const isReviewMode = payload.mode === "review";
|
|
257
|
+
|
|
258
|
+
if (isReviewMode) {
|
|
259
|
+
// Build the review output that instructs Claude Code to spawn quality-auditor
|
|
260
|
+
const reviewOutput = `Chat: \`${chatId}\`
|
|
261
|
+
|
|
262
|
+
[CLAUDE_CODE_REVIEW_REQUEST]
|
|
263
|
+
The context has been exported to: ${chatPath}
|
|
264
|
+
|
|
265
|
+
To complete this review, spawn a quality-auditor subagent with the Task tool:
|
|
266
|
+
|
|
267
|
+
Task(subagent_type="flow-next:quality-auditor", prompt="""
|
|
268
|
+
Review the code changes in this context file: ${chatPath}
|
|
269
|
+
|
|
270
|
+
Read the file, analyze the code for:
|
|
271
|
+
- Correctness - Logic errors, bugs
|
|
272
|
+
- Security - Injection, auth gaps
|
|
273
|
+
- Simplicity - Over-engineering
|
|
274
|
+
- Edge cases - Failure modes
|
|
275
|
+
|
|
276
|
+
Provide findings by severity (Critical > Major > Minor).
|
|
277
|
+
|
|
278
|
+
REQUIRED: End with exactly one verdict tag:
|
|
279
|
+
<verdict>SHIP</verdict> - Code is production-ready
|
|
280
|
+
<verdict>NEEDS_WORK</verdict> - Issues must be fixed first
|
|
281
|
+
""")
|
|
282
|
+
|
|
283
|
+
The subagent will return the review findings and verdict.
|
|
284
|
+
[/CLAUDE_CODE_REVIEW_REQUEST]`;
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
success: true,
|
|
288
|
+
data: { id: chatId, path: chatPath },
|
|
289
|
+
output: reviewOutput,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Return in the expected format: Chat: `<uuid>`
|
|
294
|
+
return {
|
|
295
|
+
success: true,
|
|
296
|
+
data: { id: chatId, path: chatPath },
|
|
297
|
+
output: `Chat: \`${chatId}\``,
|
|
298
|
+
};
|
|
299
|
+
} catch (error) {
|
|
300
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
301
|
+
return {
|
|
302
|
+
success: false,
|
|
303
|
+
error: `Failed to send chat: ${message}`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Init command - set up wdyt
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* bunx wdyt init # Interactive setup
|
|
6
|
+
* bunx wdyt init --rp-alias # Also create rp-cli alias (skip prompt)
|
|
7
|
+
* bunx wdyt init --no-alias # Skip rp-cli alias (no prompt)
|
|
8
|
+
* bunx wdyt init --global # Install globally
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { mkdirSync, existsSync, symlinkSync, unlinkSync } from "fs";
|
|
12
|
+
import { join } from "path";
|
|
13
|
+
import { homedir } from "os";
|
|
14
|
+
import { $ } from "bun";
|
|
15
|
+
import * as readline from "readline";
|
|
16
|
+
|
|
17
|
+
interface InitOptions {
|
|
18
|
+
rpAlias?: boolean;
|
|
19
|
+
noAlias?: boolean;
|
|
20
|
+
global?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Prompt user for yes/no input
|
|
25
|
+
*/
|
|
26
|
+
async function promptYesNo(question: string, defaultYes = false): Promise<boolean> {
|
|
27
|
+
const rl = readline.createInterface({
|
|
28
|
+
input: process.stdin,
|
|
29
|
+
output: process.stdout,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const hint = defaultYes ? "[Y/n]" : "[y/N]";
|
|
33
|
+
|
|
34
|
+
return new Promise((resolve) => {
|
|
35
|
+
rl.question(`${question} ${hint} `, (answer) => {
|
|
36
|
+
rl.close();
|
|
37
|
+
const trimmed = answer.trim().toLowerCase();
|
|
38
|
+
if (trimmed === "") {
|
|
39
|
+
resolve(defaultYes);
|
|
40
|
+
} else {
|
|
41
|
+
resolve(trimmed === "y" || trimmed === "yes");
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the data directory path
|
|
49
|
+
*/
|
|
50
|
+
function getDataDir(): string {
|
|
51
|
+
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
52
|
+
if (xdgDataHome) {
|
|
53
|
+
return join(xdgDataHome, "wdyt");
|
|
54
|
+
}
|
|
55
|
+
return join(homedir(), ".wdyt");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get the bin directory for user installs
|
|
60
|
+
*/
|
|
61
|
+
function getUserBinDir(): string {
|
|
62
|
+
return join(homedir(), ".local", "bin");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Check if a command exists in PATH
|
|
67
|
+
*/
|
|
68
|
+
async function commandExists(cmd: string): Promise<boolean> {
|
|
69
|
+
try {
|
|
70
|
+
await $`which ${cmd}`.quiet();
|
|
71
|
+
return true;
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Run the init command
|
|
79
|
+
*/
|
|
80
|
+
export async function initCommand(options: InitOptions): Promise<{
|
|
81
|
+
success: boolean;
|
|
82
|
+
output?: string;
|
|
83
|
+
error?: string;
|
|
84
|
+
}> {
|
|
85
|
+
const lines: string[] = [];
|
|
86
|
+
const dataDir = getDataDir();
|
|
87
|
+
const binDir = getUserBinDir();
|
|
88
|
+
|
|
89
|
+
lines.push("🔍 wdyt - Code review context builder for LLMs");
|
|
90
|
+
lines.push("");
|
|
91
|
+
|
|
92
|
+
// 1. Create data directory
|
|
93
|
+
lines.push("Setting up data directory...");
|
|
94
|
+
try {
|
|
95
|
+
mkdirSync(dataDir, { recursive: true });
|
|
96
|
+
mkdirSync(join(dataDir, "chats"), { recursive: true });
|
|
97
|
+
lines.push(` ✓ Created ${dataDir}`);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
error: `Failed to create data directory: ${error}`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 2. Check if already installed globally
|
|
106
|
+
const alreadyInstalled = await commandExists("wdyt");
|
|
107
|
+
|
|
108
|
+
if (alreadyInstalled && !options.global) {
|
|
109
|
+
lines.push("");
|
|
110
|
+
lines.push("✓ wdyt is already available in PATH");
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 3. Global install if requested
|
|
114
|
+
if (options.global) {
|
|
115
|
+
lines.push("");
|
|
116
|
+
lines.push("Installing globally...");
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
// Ensure bin directory exists
|
|
120
|
+
mkdirSync(binDir, { recursive: true });
|
|
121
|
+
|
|
122
|
+
// Get the path to the current executable or script
|
|
123
|
+
const currentExe = process.argv[1];
|
|
124
|
+
const targetPath = join(binDir, "wdyt");
|
|
125
|
+
|
|
126
|
+
// Build the binary
|
|
127
|
+
lines.push(" Building binary...");
|
|
128
|
+
const srcDir = join(import.meta.dir, "..");
|
|
129
|
+
await $`bun build ${join(srcDir, "cli.ts")} --compile --outfile ${targetPath}`.quiet();
|
|
130
|
+
|
|
131
|
+
lines.push(` ✓ Installed to ${targetPath}`);
|
|
132
|
+
|
|
133
|
+
// Check if ~/.local/bin is in PATH
|
|
134
|
+
const path = process.env.PATH || "";
|
|
135
|
+
if (!path.includes(binDir)) {
|
|
136
|
+
lines.push("");
|
|
137
|
+
lines.push(`⚠️ Add ${binDir} to your PATH:`);
|
|
138
|
+
lines.push(` echo 'export PATH="${binDir}:$PATH"' >> ~/.bashrc`);
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
lines.push(` ✗ Failed to install: ${error}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 4. Determine if we should create rp-cli alias
|
|
146
|
+
let shouldCreateAlias = options.rpAlias;
|
|
147
|
+
|
|
148
|
+
// If neither --rp-alias nor --no-alias was specified, prompt the user
|
|
149
|
+
if (!options.rpAlias && !options.noAlias) {
|
|
150
|
+
lines.push("");
|
|
151
|
+
console.log(lines.join("\n"));
|
|
152
|
+
lines.length = 0; // Clear lines since we just printed them
|
|
153
|
+
|
|
154
|
+
console.log("");
|
|
155
|
+
console.log("The rp-cli alias enables compatibility with flowctl/flow-next.");
|
|
156
|
+
shouldCreateAlias = await promptYesNo("Create rp-cli alias?", true);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Create rp-cli alias if requested or confirmed
|
|
160
|
+
if (shouldCreateAlias) {
|
|
161
|
+
lines.push("");
|
|
162
|
+
lines.push("Creating rp-cli alias (for flowctl compatibility)...");
|
|
163
|
+
|
|
164
|
+
const rpCliPath = join(binDir, "rp-cli");
|
|
165
|
+
const secondOpinionPath = join(binDir, "wdyt");
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// Ensure bin directory exists
|
|
169
|
+
mkdirSync(binDir, { recursive: true });
|
|
170
|
+
|
|
171
|
+
// Remove existing symlink if present
|
|
172
|
+
if (existsSync(rpCliPath)) {
|
|
173
|
+
unlinkSync(rpCliPath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check if wdyt binary exists
|
|
177
|
+
if (existsSync(secondOpinionPath)) {
|
|
178
|
+
symlinkSync(secondOpinionPath, rpCliPath);
|
|
179
|
+
lines.push(` ✓ Created symlink: rp-cli -> wdyt`);
|
|
180
|
+
} else {
|
|
181
|
+
// If not installed globally, create the binary first
|
|
182
|
+
lines.push(" Building rp-cli binary...");
|
|
183
|
+
const srcDir = join(import.meta.dir, "..");
|
|
184
|
+
await $`bun build ${join(srcDir, "cli.ts")} --compile --outfile ${rpCliPath}`.quiet();
|
|
185
|
+
lines.push(` ✓ Installed rp-cli to ${rpCliPath}`);
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
lines.push(` ✗ Failed to create alias: ${error}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 5. Summary
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push("Setup complete! 🎉");
|
|
195
|
+
lines.push("");
|
|
196
|
+
lines.push("Usage:");
|
|
197
|
+
lines.push(" wdyt -e 'windows' # List windows");
|
|
198
|
+
lines.push(" wdyt -w 1 -e 'builder {}' # Create tab");
|
|
199
|
+
lines.push(" wdyt -w 1 -t <id> -e 'select add file.ts'");
|
|
200
|
+
lines.push("");
|
|
201
|
+
|
|
202
|
+
if (!shouldCreateAlias && !options.noAlias) {
|
|
203
|
+
lines.push("Tip: For flowctl/flow-next compatibility, run:");
|
|
204
|
+
lines.push(" bunx wdyt init --rp-alias");
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
success: true,
|
|
209
|
+
output: lines.join("\n"),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Parse init command arguments
|
|
215
|
+
*/
|
|
216
|
+
export function parseInitArgs(args: string[]): InitOptions {
|
|
217
|
+
return {
|
|
218
|
+
rpAlias: args.includes("--rp-alias") || args.includes("--rp"),
|
|
219
|
+
noAlias: args.includes("--no-alias") || args.includes("--no-rp"),
|
|
220
|
+
global: args.includes("--global") || args.includes("-g"),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prompt commands - get, set (via call), export
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* - prompt get: returns current prompt text
|
|
6
|
+
* - call prompt {"op":"set","text":"..."}: sets prompt text
|
|
7
|
+
* - prompt export <file>: writes prompt to file
|
|
8
|
+
*
|
|
9
|
+
* Compatible with flowctl.py:
|
|
10
|
+
* - cmd_rp_prompt_get (line 3924): prompt get
|
|
11
|
+
* - cmd_rp_prompt_set (line 3930): call prompt {"op":"set","text":"..."}
|
|
12
|
+
* - cmd_rp_prompt_export (line 3986): prompt export <file>
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { resolve } from "path";
|
|
16
|
+
import { homedir } from "os";
|
|
17
|
+
import { getTab, updateTab } from "../state";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Validate that a file path is safe for writing
|
|
21
|
+
* Prevents path traversal attacks by ensuring path is within allowed directories
|
|
22
|
+
*/
|
|
23
|
+
function isPathSafe(filePath: string): boolean {
|
|
24
|
+
const resolved = resolve(filePath);
|
|
25
|
+
const home = homedir();
|
|
26
|
+
const cwd = process.cwd();
|
|
27
|
+
const tmp = "/tmp";
|
|
28
|
+
|
|
29
|
+
// Allow paths within home directory, current working directory, or /tmp
|
|
30
|
+
return (
|
|
31
|
+
resolved.startsWith(home) ||
|
|
32
|
+
resolved.startsWith(cwd) ||
|
|
33
|
+
resolved.startsWith(tmp)
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Prompt get response
|
|
39
|
+
*/
|
|
40
|
+
export interface PromptGetResponse {
|
|
41
|
+
prompt: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Prompt set payload
|
|
46
|
+
*/
|
|
47
|
+
export interface PromptSetPayload {
|
|
48
|
+
op: "set";
|
|
49
|
+
text: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get prompt text for a tab
|
|
54
|
+
*
|
|
55
|
+
* @param windowId - Window ID
|
|
56
|
+
* @param tabId - Tab ID
|
|
57
|
+
* @returns Current prompt text (empty string if none)
|
|
58
|
+
*/
|
|
59
|
+
export async function promptGetCommand(
|
|
60
|
+
windowId: number,
|
|
61
|
+
tabId: string
|
|
62
|
+
): Promise<{
|
|
63
|
+
success: boolean;
|
|
64
|
+
data?: PromptGetResponse;
|
|
65
|
+
output?: string;
|
|
66
|
+
error?: string;
|
|
67
|
+
}> {
|
|
68
|
+
try {
|
|
69
|
+
const tab = await getTab(windowId, tabId);
|
|
70
|
+
|
|
71
|
+
// Return just the prompt text, no JSON wrapping when not --raw-json
|
|
72
|
+
return {
|
|
73
|
+
success: true,
|
|
74
|
+
data: { prompt: tab.prompt },
|
|
75
|
+
output: tab.prompt,
|
|
76
|
+
};
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
79
|
+
return {
|
|
80
|
+
success: false,
|
|
81
|
+
error: `Failed to get prompt: ${message}`,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Set prompt text for a tab
|
|
88
|
+
*
|
|
89
|
+
* Called as: call prompt {"op":"set","text":"..."}
|
|
90
|
+
*
|
|
91
|
+
* @param windowId - Window ID
|
|
92
|
+
* @param tabId - Tab ID
|
|
93
|
+
* @param payload - JSON payload with op and text
|
|
94
|
+
* @returns Success status
|
|
95
|
+
*/
|
|
96
|
+
export async function promptSetCommand(
|
|
97
|
+
windowId: number,
|
|
98
|
+
tabId: string,
|
|
99
|
+
payload: string
|
|
100
|
+
): Promise<{
|
|
101
|
+
success: boolean;
|
|
102
|
+
data?: { success: boolean };
|
|
103
|
+
output?: string;
|
|
104
|
+
error?: string;
|
|
105
|
+
}> {
|
|
106
|
+
try {
|
|
107
|
+
// Parse the JSON payload
|
|
108
|
+
const data = JSON.parse(payload) as PromptSetPayload;
|
|
109
|
+
|
|
110
|
+
if (data.op !== "set") {
|
|
111
|
+
return {
|
|
112
|
+
success: false,
|
|
113
|
+
error: `Unknown prompt operation: ${data.op}`,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (typeof data.text !== "string") {
|
|
118
|
+
return {
|
|
119
|
+
success: false,
|
|
120
|
+
error: "Prompt text must be a string",
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Update the tab's prompt
|
|
125
|
+
await updateTab(windowId, tabId, { prompt: data.text });
|
|
126
|
+
|
|
127
|
+
return {
|
|
128
|
+
success: true,
|
|
129
|
+
data: { success: true },
|
|
130
|
+
output: "OK",
|
|
131
|
+
};
|
|
132
|
+
} catch (error) {
|
|
133
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
134
|
+
return {
|
|
135
|
+
success: false,
|
|
136
|
+
error: `Failed to set prompt: ${message}`,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Export prompt to a file
|
|
143
|
+
*
|
|
144
|
+
* Called as: prompt export <file>
|
|
145
|
+
*
|
|
146
|
+
* @param windowId - Window ID
|
|
147
|
+
* @param tabId - Tab ID
|
|
148
|
+
* @param filePath - Path to write prompt to
|
|
149
|
+
* @returns Success status
|
|
150
|
+
*/
|
|
151
|
+
export async function promptExportCommand(
|
|
152
|
+
windowId: number,
|
|
153
|
+
tabId: string,
|
|
154
|
+
filePath: string
|
|
155
|
+
): Promise<{
|
|
156
|
+
success: boolean;
|
|
157
|
+
data?: { path: string };
|
|
158
|
+
output?: string;
|
|
159
|
+
error?: string;
|
|
160
|
+
}> {
|
|
161
|
+
try {
|
|
162
|
+
// Validate path to prevent path traversal attacks
|
|
163
|
+
if (!isPathSafe(filePath)) {
|
|
164
|
+
return {
|
|
165
|
+
success: false,
|
|
166
|
+
error: `Path not allowed: ${filePath}. Must be within home directory, current directory, or /tmp`,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const tab = await getTab(windowId, tabId);
|
|
171
|
+
const resolvedPath = resolve(filePath);
|
|
172
|
+
|
|
173
|
+
// Write prompt to file
|
|
174
|
+
await Bun.write(resolvedPath, tab.prompt);
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
success: true,
|
|
178
|
+
data: { path: resolvedPath },
|
|
179
|
+
output: `Exported to ${resolvedPath}`,
|
|
180
|
+
};
|
|
181
|
+
} catch (error) {
|
|
182
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
183
|
+
return {
|
|
184
|
+
success: false,
|
|
185
|
+
error: `Failed to export prompt: ${message}`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
}
|