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,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Select commands - get, add
|
|
3
|
+
*
|
|
4
|
+
* Commands:
|
|
5
|
+
* - select get: returns selected file paths (newline-separated)
|
|
6
|
+
* - select add <paths>: adds files to selection
|
|
7
|
+
*
|
|
8
|
+
* Compatible with flowctl.py:
|
|
9
|
+
* - cmd_rp_select_get (line 3946): select get
|
|
10
|
+
* - cmd_rp_select_add (line 3955): select add <paths>
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync } from "fs";
|
|
14
|
+
import { resolve } from "path";
|
|
15
|
+
import { getTab, updateTab, getWindow } from "../state";
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Select get response
|
|
19
|
+
*/
|
|
20
|
+
export interface SelectGetResponse {
|
|
21
|
+
files: string[];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get selected files for a tab
|
|
26
|
+
*
|
|
27
|
+
* @param windowId - Window ID
|
|
28
|
+
* @param tabId - Tab ID
|
|
29
|
+
* @returns Newline-separated file paths
|
|
30
|
+
*/
|
|
31
|
+
export async function selectGetCommand(
|
|
32
|
+
windowId: number,
|
|
33
|
+
tabId: string
|
|
34
|
+
): Promise<{
|
|
35
|
+
success: boolean;
|
|
36
|
+
data?: SelectGetResponse;
|
|
37
|
+
output?: string;
|
|
38
|
+
error?: string;
|
|
39
|
+
}> {
|
|
40
|
+
try {
|
|
41
|
+
const tab = await getTab(windowId, tabId);
|
|
42
|
+
|
|
43
|
+
// Return newline-separated paths for non-JSON output
|
|
44
|
+
const output = tab.selectedFiles.join("\n");
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
success: true,
|
|
48
|
+
data: { files: tab.selectedFiles },
|
|
49
|
+
output,
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
error: `Failed to get selection: ${message}`,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse shell-quoted paths from the arguments string
|
|
62
|
+
*
|
|
63
|
+
* Handles paths that may be quoted with single quotes (from shlex.quote)
|
|
64
|
+
* Example: "'path/to/file.ts' 'another file.ts'"
|
|
65
|
+
*/
|
|
66
|
+
function parsePaths(argsString: string): string[] {
|
|
67
|
+
const paths: string[] = [];
|
|
68
|
+
let remaining = argsString.trim();
|
|
69
|
+
|
|
70
|
+
while (remaining.length > 0) {
|
|
71
|
+
remaining = remaining.trimStart();
|
|
72
|
+
|
|
73
|
+
if (remaining.startsWith("'")) {
|
|
74
|
+
// Single-quoted path
|
|
75
|
+
const endQuote = remaining.indexOf("'", 1);
|
|
76
|
+
if (endQuote === -1) {
|
|
77
|
+
// Unterminated quote, take rest
|
|
78
|
+
paths.push(remaining.slice(1));
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
paths.push(remaining.slice(1, endQuote));
|
|
82
|
+
remaining = remaining.slice(endQuote + 1);
|
|
83
|
+
} else if (remaining.startsWith('"')) {
|
|
84
|
+
// Double-quoted path
|
|
85
|
+
const endQuote = remaining.indexOf('"', 1);
|
|
86
|
+
if (endQuote === -1) {
|
|
87
|
+
paths.push(remaining.slice(1));
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
paths.push(remaining.slice(1, endQuote));
|
|
91
|
+
remaining = remaining.slice(endQuote + 1);
|
|
92
|
+
} else {
|
|
93
|
+
// Unquoted path - ends at whitespace
|
|
94
|
+
const spaceIndex = remaining.search(/\s/);
|
|
95
|
+
if (spaceIndex === -1) {
|
|
96
|
+
paths.push(remaining);
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
paths.push(remaining.slice(0, spaceIndex));
|
|
100
|
+
remaining = remaining.slice(spaceIndex);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return paths.filter((p) => p.length > 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Add files to selection for a tab
|
|
109
|
+
*
|
|
110
|
+
* @param windowId - Window ID
|
|
111
|
+
* @param tabId - Tab ID
|
|
112
|
+
* @param argsString - Space-separated file paths (may be shell-quoted)
|
|
113
|
+
* @returns Success status with count of added files
|
|
114
|
+
*/
|
|
115
|
+
export async function selectAddCommand(
|
|
116
|
+
windowId: number,
|
|
117
|
+
tabId: string,
|
|
118
|
+
argsString: string
|
|
119
|
+
): Promise<{
|
|
120
|
+
success: boolean;
|
|
121
|
+
data?: { added: number; total: number };
|
|
122
|
+
output?: string;
|
|
123
|
+
error?: string;
|
|
124
|
+
}> {
|
|
125
|
+
try {
|
|
126
|
+
// Parse the paths from the args string
|
|
127
|
+
const pathsToAdd = parsePaths(argsString);
|
|
128
|
+
|
|
129
|
+
if (pathsToAdd.length === 0) {
|
|
130
|
+
return {
|
|
131
|
+
success: false,
|
|
132
|
+
error: "select add requires at least one path",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Get window to determine root paths for resolving relative paths
|
|
137
|
+
const window = await getWindow(windowId);
|
|
138
|
+
const rootPath = window.rootFolderPaths[0] || process.cwd();
|
|
139
|
+
|
|
140
|
+
// Get current selection
|
|
141
|
+
const tab = await getTab(windowId, tabId);
|
|
142
|
+
const existingFiles = new Set(tab.selectedFiles);
|
|
143
|
+
|
|
144
|
+
// Process each path
|
|
145
|
+
let addedCount = 0;
|
|
146
|
+
for (const path of pathsToAdd) {
|
|
147
|
+
// Resolve to absolute path if relative
|
|
148
|
+
const absolutePath = resolve(rootPath, path);
|
|
149
|
+
|
|
150
|
+
// Skip if already selected (deduplicate)
|
|
151
|
+
if (existingFiles.has(absolutePath)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check if file exists - silently skip non-existent files
|
|
156
|
+
if (!existsSync(absolutePath)) {
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
existingFiles.add(absolutePath);
|
|
161
|
+
addedCount++;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Update the tab with new selection
|
|
165
|
+
const newSelection = Array.from(existingFiles);
|
|
166
|
+
await updateTab(windowId, tabId, { selectedFiles: newSelection });
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
success: true,
|
|
170
|
+
data: { added: addedCount, total: newSelection.length },
|
|
171
|
+
output: `Added ${addedCount} file(s), total: ${newSelection.length}`,
|
|
172
|
+
};
|
|
173
|
+
} catch (error) {
|
|
174
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
175
|
+
return {
|
|
176
|
+
success: false,
|
|
177
|
+
error: `Failed to add to selection: ${message}`,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows command - list all windows
|
|
3
|
+
*
|
|
4
|
+
* Returns JSON: {windows: [{windowID, rootFolderPaths}]}
|
|
5
|
+
* Compatible with flowctl.py parsing at line 215-231
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { getWindows } from "../state";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Window data formatted for flowctl.py compatibility
|
|
12
|
+
* Uses windowID (not id) to match expected format
|
|
13
|
+
*/
|
|
14
|
+
export interface WindowOutput {
|
|
15
|
+
windowID: number;
|
|
16
|
+
rootFolderPaths: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Windows command response
|
|
21
|
+
*/
|
|
22
|
+
export interface WindowsResponse {
|
|
23
|
+
windows: WindowOutput[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Execute the windows command
|
|
28
|
+
* Returns all windows with their IDs and root folder paths
|
|
29
|
+
*/
|
|
30
|
+
export async function windowsCommand(): Promise<{
|
|
31
|
+
success: boolean;
|
|
32
|
+
data?: WindowsResponse;
|
|
33
|
+
error?: string;
|
|
34
|
+
}> {
|
|
35
|
+
try {
|
|
36
|
+
const windows = await getWindows();
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
success: true,
|
|
40
|
+
data: {
|
|
41
|
+
windows: windows.map((w) => ({
|
|
42
|
+
windowID: w.id,
|
|
43
|
+
rootFolderPaths: w.rootFolderPaths,
|
|
44
|
+
})),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
49
|
+
return {
|
|
50
|
+
success: false,
|
|
51
|
+
error: `Failed to get windows: ${message}`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for state management layer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { rm } from "fs/promises";
|
|
8
|
+
import {
|
|
9
|
+
loadState,
|
|
10
|
+
saveState,
|
|
11
|
+
getWindow,
|
|
12
|
+
getWindows,
|
|
13
|
+
createTab,
|
|
14
|
+
getTab,
|
|
15
|
+
updateTab,
|
|
16
|
+
deleteTab,
|
|
17
|
+
createWindow,
|
|
18
|
+
ensureState,
|
|
19
|
+
} from "./state";
|
|
20
|
+
|
|
21
|
+
// Use a test-specific directory
|
|
22
|
+
const TEST_DIR = join(import.meta.dir, "..", ".test-state");
|
|
23
|
+
|
|
24
|
+
beforeEach(async () => {
|
|
25
|
+
// Set XDG_DATA_HOME to test directory
|
|
26
|
+
process.env.XDG_DATA_HOME = TEST_DIR;
|
|
27
|
+
|
|
28
|
+
// Clean up any existing test state
|
|
29
|
+
try {
|
|
30
|
+
await rm(TEST_DIR, { recursive: true, force: true });
|
|
31
|
+
} catch {
|
|
32
|
+
// Directory might not exist
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
afterEach(async () => {
|
|
37
|
+
// Clean up test directory
|
|
38
|
+
try {
|
|
39
|
+
await rm(TEST_DIR, { recursive: true, force: true });
|
|
40
|
+
} catch {
|
|
41
|
+
// Directory might not exist
|
|
42
|
+
}
|
|
43
|
+
delete process.env.XDG_DATA_HOME;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe("State management", () => {
|
|
47
|
+
test("loadState creates default state if none exists", async () => {
|
|
48
|
+
const state = await loadState();
|
|
49
|
+
|
|
50
|
+
expect(state).toBeDefined();
|
|
51
|
+
expect(state.version).toBe(1);
|
|
52
|
+
expect(state.windows).toHaveLength(1);
|
|
53
|
+
expect(state.windows[0].id).toBe(1);
|
|
54
|
+
expect(state.windows[0].tabs).toHaveLength(0);
|
|
55
|
+
expect(state.windows[0].rootFolderPaths).toEqual([]);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("state survives process restarts (save and reload)", async () => {
|
|
59
|
+
// Initial load creates default
|
|
60
|
+
await ensureState();
|
|
61
|
+
|
|
62
|
+
// Create a tab
|
|
63
|
+
const tab = await createTab(1);
|
|
64
|
+
expect(tab.id).toBeDefined();
|
|
65
|
+
expect(tab.prompt).toBe("");
|
|
66
|
+
expect(tab.selectedFiles).toEqual([]);
|
|
67
|
+
|
|
68
|
+
// Update the tab
|
|
69
|
+
await updateTab(1, tab.id, {
|
|
70
|
+
prompt: "test prompt",
|
|
71
|
+
selectedFiles: ["file1.ts", "file2.ts"],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Reload state (simulating process restart)
|
|
75
|
+
const reloadedState = await loadState();
|
|
76
|
+
const reloadedWindow = reloadedState.windows.find((w) => w.id === 1);
|
|
77
|
+
expect(reloadedWindow).toBeDefined();
|
|
78
|
+
expect(reloadedWindow!.tabs).toHaveLength(1);
|
|
79
|
+
|
|
80
|
+
const reloadedTab = reloadedWindow!.tabs[0];
|
|
81
|
+
expect(reloadedTab.id).toBe(tab.id);
|
|
82
|
+
expect(reloadedTab.prompt).toBe("test prompt");
|
|
83
|
+
expect(reloadedTab.selectedFiles).toEqual(["file1.ts", "file2.ts"]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("getWindow returns window or throws", async () => {
|
|
87
|
+
await ensureState();
|
|
88
|
+
|
|
89
|
+
// Window 1 should exist by default
|
|
90
|
+
const window = await getWindow(1);
|
|
91
|
+
expect(window.id).toBe(1);
|
|
92
|
+
|
|
93
|
+
// Window 999 should not exist
|
|
94
|
+
await expect(getWindow(999)).rejects.toThrow("Window 999 not found");
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("createTab returns new tab with UUID", async () => {
|
|
98
|
+
await ensureState();
|
|
99
|
+
|
|
100
|
+
const tab = await createTab(1);
|
|
101
|
+
|
|
102
|
+
expect(tab.id).toBeDefined();
|
|
103
|
+
expect(tab.id.length).toBe(36); // UUID length
|
|
104
|
+
expect(tab.prompt).toBe("");
|
|
105
|
+
expect(tab.selectedFiles).toEqual([]);
|
|
106
|
+
expect(tab.createdAt).toBeDefined();
|
|
107
|
+
|
|
108
|
+
// Verify it was persisted
|
|
109
|
+
const window = await getWindow(1);
|
|
110
|
+
expect(window.tabs).toHaveLength(1);
|
|
111
|
+
expect(window.tabs[0].id).toBe(tab.id);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("updateTab persists changes", async () => {
|
|
115
|
+
await ensureState();
|
|
116
|
+
|
|
117
|
+
const tab = await createTab(1);
|
|
118
|
+
|
|
119
|
+
// Update prompt only
|
|
120
|
+
const updated1 = await updateTab(1, tab.id, { prompt: "new prompt" });
|
|
121
|
+
expect(updated1.prompt).toBe("new prompt");
|
|
122
|
+
expect(updated1.selectedFiles).toEqual([]);
|
|
123
|
+
|
|
124
|
+
// Update selectedFiles only
|
|
125
|
+
const updated2 = await updateTab(1, tab.id, {
|
|
126
|
+
selectedFiles: ["a.ts", "b.ts"],
|
|
127
|
+
});
|
|
128
|
+
expect(updated2.prompt).toBe("new prompt");
|
|
129
|
+
expect(updated2.selectedFiles).toEqual(["a.ts", "b.ts"]);
|
|
130
|
+
|
|
131
|
+
// Verify persistence
|
|
132
|
+
const reloadedTab = await getTab(1, tab.id);
|
|
133
|
+
expect(reloadedTab.prompt).toBe("new prompt");
|
|
134
|
+
expect(reloadedTab.selectedFiles).toEqual(["a.ts", "b.ts"]);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("createTab throws for non-existent window", async () => {
|
|
138
|
+
await ensureState();
|
|
139
|
+
|
|
140
|
+
await expect(createTab(999)).rejects.toThrow("Window 999 not found");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("updateTab throws for non-existent tab", async () => {
|
|
144
|
+
await ensureState();
|
|
145
|
+
|
|
146
|
+
await expect(updateTab(1, "fake-id", { prompt: "test" })).rejects.toThrow(
|
|
147
|
+
"Tab fake-id not found"
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("deleteTab removes tab from window", async () => {
|
|
152
|
+
await ensureState();
|
|
153
|
+
|
|
154
|
+
const tab = await createTab(1);
|
|
155
|
+
const window1 = await getWindow(1);
|
|
156
|
+
expect(window1.tabs).toHaveLength(1);
|
|
157
|
+
|
|
158
|
+
await deleteTab(1, tab.id);
|
|
159
|
+
|
|
160
|
+
const window2 = await getWindow(1);
|
|
161
|
+
expect(window2.tabs).toHaveLength(0);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("createWindow adds new window with incremented ID", async () => {
|
|
165
|
+
await ensureState();
|
|
166
|
+
|
|
167
|
+
const newWindow = await createWindow(["/path/to/project"]);
|
|
168
|
+
|
|
169
|
+
expect(newWindow.id).toBe(2);
|
|
170
|
+
expect(newWindow.rootFolderPaths).toEqual(["/path/to/project"]);
|
|
171
|
+
expect(newWindow.tabs).toHaveLength(0);
|
|
172
|
+
|
|
173
|
+
const windows = await getWindows();
|
|
174
|
+
expect(windows).toHaveLength(2);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("getWindows returns all windows", async () => {
|
|
178
|
+
await ensureState();
|
|
179
|
+
|
|
180
|
+
const windows = await getWindows();
|
|
181
|
+
expect(windows).toHaveLength(1);
|
|
182
|
+
expect(windows[0].id).toBe(1);
|
|
183
|
+
});
|
|
184
|
+
});
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State management layer for wdyt
|
|
3
|
+
*
|
|
4
|
+
* Persists windows, tabs, and selections to disk.
|
|
5
|
+
* State is stored in ~/.wdyt/state.json (or XDG_DATA_HOME/wdyt/state.json)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { mkdirSync, existsSync, renameSync } from "fs";
|
|
11
|
+
import type { StateFile, Window, Tab, TabUpdate } from "./types";
|
|
12
|
+
|
|
13
|
+
const STATE_VERSION = 1;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the data directory path
|
|
17
|
+
* Uses XDG_DATA_HOME if set, otherwise falls back to ~/.wdyt
|
|
18
|
+
*/
|
|
19
|
+
function getDataDir(): string {
|
|
20
|
+
const xdgDataHome = process.env.XDG_DATA_HOME;
|
|
21
|
+
if (xdgDataHome) {
|
|
22
|
+
return join(xdgDataHome, "wdyt");
|
|
23
|
+
}
|
|
24
|
+
return join(homedir(), ".wdyt");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the state file path
|
|
29
|
+
*/
|
|
30
|
+
function getStatePath(): string {
|
|
31
|
+
return join(getDataDir(), "state.json");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create default state with a single window
|
|
36
|
+
*/
|
|
37
|
+
function createDefaultState(): StateFile {
|
|
38
|
+
return {
|
|
39
|
+
version: STATE_VERSION,
|
|
40
|
+
windows: [
|
|
41
|
+
{
|
|
42
|
+
id: 1,
|
|
43
|
+
rootFolderPaths: [],
|
|
44
|
+
tabs: [],
|
|
45
|
+
},
|
|
46
|
+
],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate a UUID v4 for tab IDs
|
|
52
|
+
*/
|
|
53
|
+
function generateUUID(): string {
|
|
54
|
+
return crypto.randomUUID();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Load state from disk, creating default if not exists
|
|
59
|
+
*/
|
|
60
|
+
export async function loadState(): Promise<StateFile> {
|
|
61
|
+
const statePath = getStatePath();
|
|
62
|
+
|
|
63
|
+
// Check if file exists first
|
|
64
|
+
if (!existsSync(statePath)) {
|
|
65
|
+
return createDefaultState();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const file = Bun.file(statePath);
|
|
70
|
+
const content = await file.text();
|
|
71
|
+
const state = JSON.parse(content) as StateFile;
|
|
72
|
+
|
|
73
|
+
// Validate basic structure
|
|
74
|
+
if (!state || typeof state !== "object" || !Array.isArray(state.windows)) {
|
|
75
|
+
throw new Error("Invalid state structure");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Validate and migrate if needed
|
|
79
|
+
if (!state.version || state.version < STATE_VERSION) {
|
|
80
|
+
// Future: handle migrations
|
|
81
|
+
state.version = STATE_VERSION;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return state;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
// State file exists but is corrupted - backup and start fresh
|
|
87
|
+
const backupPath = `${statePath}.backup.${Date.now()}`;
|
|
88
|
+
try {
|
|
89
|
+
renameSync(statePath, backupPath);
|
|
90
|
+
console.error(`Warning: State file corrupted, backed up to ${backupPath}`);
|
|
91
|
+
} catch {
|
|
92
|
+
console.error(`Warning: Could not backup corrupted state file: ${error}`);
|
|
93
|
+
}
|
|
94
|
+
return createDefaultState();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Save state to disk
|
|
100
|
+
*/
|
|
101
|
+
export async function saveState(state: StateFile): Promise<void> {
|
|
102
|
+
const statePath = getStatePath();
|
|
103
|
+
const dataDir = getDataDir();
|
|
104
|
+
|
|
105
|
+
// Ensure directory exists using proper mkdir
|
|
106
|
+
mkdirSync(dataDir, { recursive: true });
|
|
107
|
+
|
|
108
|
+
// Write state file atomically
|
|
109
|
+
const content = JSON.stringify(state, null, 2);
|
|
110
|
+
await Bun.write(statePath, content);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get a window by ID
|
|
115
|
+
* @throws Error if window not found
|
|
116
|
+
*/
|
|
117
|
+
export async function getWindow(id: number): Promise<Window> {
|
|
118
|
+
const state = await loadState();
|
|
119
|
+
const window = state.windows.find((w) => w.id === id);
|
|
120
|
+
|
|
121
|
+
if (!window) {
|
|
122
|
+
throw new Error(`Window ${id} not found`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return window;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Get all windows
|
|
130
|
+
*/
|
|
131
|
+
export async function getWindows(): Promise<Window[]> {
|
|
132
|
+
const state = await loadState();
|
|
133
|
+
return state.windows;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Create a new tab in a window
|
|
138
|
+
* @returns The newly created tab
|
|
139
|
+
*/
|
|
140
|
+
export async function createTab(windowId: number): Promise<Tab> {
|
|
141
|
+
const state = await loadState();
|
|
142
|
+
const window = state.windows.find((w) => w.id === windowId);
|
|
143
|
+
|
|
144
|
+
if (!window) {
|
|
145
|
+
throw new Error(`Window ${windowId} not found`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const tab: Tab = {
|
|
149
|
+
id: generateUUID(),
|
|
150
|
+
prompt: "",
|
|
151
|
+
selectedFiles: [],
|
|
152
|
+
createdAt: new Date().toISOString(),
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
window.tabs.push(tab);
|
|
156
|
+
await saveState(state);
|
|
157
|
+
|
|
158
|
+
return tab;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Get a tab by ID from a window
|
|
163
|
+
* @throws Error if window or tab not found
|
|
164
|
+
*/
|
|
165
|
+
export async function getTab(windowId: number, tabId: string): Promise<Tab> {
|
|
166
|
+
const window = await getWindow(windowId);
|
|
167
|
+
const tab = window.tabs.find((t) => t.id === tabId);
|
|
168
|
+
|
|
169
|
+
if (!tab) {
|
|
170
|
+
throw new Error(`Tab ${tabId} not found in window ${windowId}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return tab;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Update a tab in a window
|
|
178
|
+
* @returns The updated tab
|
|
179
|
+
*/
|
|
180
|
+
export async function updateTab(
|
|
181
|
+
windowId: number,
|
|
182
|
+
tabId: string,
|
|
183
|
+
update: TabUpdate
|
|
184
|
+
): Promise<Tab> {
|
|
185
|
+
const state = await loadState();
|
|
186
|
+
const window = state.windows.find((w) => w.id === windowId);
|
|
187
|
+
|
|
188
|
+
if (!window) {
|
|
189
|
+
throw new Error(`Window ${windowId} not found`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const tabIndex = window.tabs.findIndex((t) => t.id === tabId);
|
|
193
|
+
if (tabIndex === -1) {
|
|
194
|
+
throw new Error(`Tab ${tabId} not found in window ${windowId}`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Merge update into existing tab
|
|
198
|
+
const tab = window.tabs[tabIndex];
|
|
199
|
+
if (update.prompt !== undefined) {
|
|
200
|
+
tab.prompt = update.prompt;
|
|
201
|
+
}
|
|
202
|
+
if (update.selectedFiles !== undefined) {
|
|
203
|
+
tab.selectedFiles = update.selectedFiles;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await saveState(state);
|
|
207
|
+
|
|
208
|
+
return tab;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Delete a tab from a window
|
|
213
|
+
*/
|
|
214
|
+
export async function deleteTab(windowId: number, tabId: string): Promise<void> {
|
|
215
|
+
const state = await loadState();
|
|
216
|
+
const window = state.windows.find((w) => w.id === windowId);
|
|
217
|
+
|
|
218
|
+
if (!window) {
|
|
219
|
+
throw new Error(`Window ${windowId} not found`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const tabIndex = window.tabs.findIndex((t) => t.id === tabId);
|
|
223
|
+
if (tabIndex === -1) {
|
|
224
|
+
throw new Error(`Tab ${tabId} not found in window ${windowId}`);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
window.tabs.splice(tabIndex, 1);
|
|
228
|
+
await saveState(state);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Create a new window
|
|
233
|
+
* @returns The newly created window
|
|
234
|
+
*/
|
|
235
|
+
export async function createWindow(
|
|
236
|
+
rootFolderPaths: string[] = []
|
|
237
|
+
): Promise<Window> {
|
|
238
|
+
const state = await loadState();
|
|
239
|
+
|
|
240
|
+
// Find the next available ID
|
|
241
|
+
const maxId = state.windows.reduce((max, w) => Math.max(max, w.id), 0);
|
|
242
|
+
|
|
243
|
+
const window: Window = {
|
|
244
|
+
id: maxId + 1,
|
|
245
|
+
rootFolderPaths,
|
|
246
|
+
tabs: [],
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
state.windows.push(window);
|
|
250
|
+
await saveState(state);
|
|
251
|
+
|
|
252
|
+
return window;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Update window root folder paths
|
|
257
|
+
*/
|
|
258
|
+
export async function updateWindowPaths(
|
|
259
|
+
windowId: number,
|
|
260
|
+
rootFolderPaths: string[]
|
|
261
|
+
): Promise<Window> {
|
|
262
|
+
const state = await loadState();
|
|
263
|
+
const window = state.windows.find((w) => w.id === windowId);
|
|
264
|
+
|
|
265
|
+
if (!window) {
|
|
266
|
+
throw new Error(`Window ${windowId} not found`);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
window.rootFolderPaths = rootFolderPaths;
|
|
270
|
+
await saveState(state);
|
|
271
|
+
|
|
272
|
+
return window;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Ensure state file exists, creating if necessary
|
|
277
|
+
* Called on startup to initialize state
|
|
278
|
+
*/
|
|
279
|
+
export async function ensureState(): Promise<void> {
|
|
280
|
+
const state = await loadState();
|
|
281
|
+
await saveState(state);
|
|
282
|
+
}
|