@evolve.labs/devflow 0.8.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/.claude/commands/agents/architect.md +1162 -0
- package/.claude/commands/agents/architect.meta.yaml +124 -0
- package/.claude/commands/agents/builder.md +1432 -0
- package/.claude/commands/agents/builder.meta.yaml +117 -0
- package/.claude/commands/agents/chronicler.md +633 -0
- package/.claude/commands/agents/chronicler.meta.yaml +217 -0
- package/.claude/commands/agents/guardian.md +456 -0
- package/.claude/commands/agents/guardian.meta.yaml +127 -0
- package/.claude/commands/agents/strategist.md +483 -0
- package/.claude/commands/agents/strategist.meta.yaml +158 -0
- package/.claude/commands/agents/system-designer.md +1137 -0
- package/.claude/commands/agents/system-designer.meta.yaml +156 -0
- package/.claude/commands/devflow-help.md +93 -0
- package/.claude/commands/devflow-status.md +60 -0
- package/.claude/commands/quick/create-adr.md +82 -0
- package/.claude/commands/quick/new-feature.md +57 -0
- package/.claude/commands/quick/security-check.md +54 -0
- package/.claude/commands/quick/system-design.md +58 -0
- package/.claude_project +52 -0
- package/.devflow/agents/architect.meta.yaml +122 -0
- package/.devflow/agents/builder.meta.yaml +116 -0
- package/.devflow/agents/chronicler.meta.yaml +222 -0
- package/.devflow/agents/guardian.meta.yaml +127 -0
- package/.devflow/agents/strategist.meta.yaml +158 -0
- package/.devflow/agents/system-designer.meta.yaml +265 -0
- package/.devflow/project.yaml +242 -0
- package/.gitignore-template +84 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/devflow.js +54 -0
- package/lib/autopilot.js +235 -0
- package/lib/autopilotConstants.js +213 -0
- package/lib/constants.js +95 -0
- package/lib/init.js +200 -0
- package/lib/update.js +181 -0
- package/lib/utils.js +157 -0
- package/lib/web.js +119 -0
- package/package.json +57 -0
- package/web/CHANGELOG.md +192 -0
- package/web/README.md +156 -0
- package/web/app/api/autopilot/execute/route.ts +102 -0
- package/web/app/api/autopilot/terminal-execute/route.ts +124 -0
- package/web/app/api/files/route.ts +280 -0
- package/web/app/api/files/tree/route.ts +160 -0
- package/web/app/api/git/route.ts +201 -0
- package/web/app/api/health/route.ts +94 -0
- package/web/app/api/project/open/route.ts +134 -0
- package/web/app/api/search/route.ts +247 -0
- package/web/app/api/specs/route.ts +405 -0
- package/web/app/api/terminal/route.ts +222 -0
- package/web/app/globals.css +160 -0
- package/web/app/ide/layout.tsx +43 -0
- package/web/app/ide/page.tsx +216 -0
- package/web/app/layout.tsx +34 -0
- package/web/app/page.tsx +303 -0
- package/web/components/agents/AgentIcons.tsx +281 -0
- package/web/components/autopilot/AutopilotConfigModal.tsx +245 -0
- package/web/components/autopilot/AutopilotPanel.tsx +299 -0
- package/web/components/dashboard/DashboardPanel.tsx +393 -0
- package/web/components/editor/Breadcrumbs.tsx +134 -0
- package/web/components/editor/EditorPanel.tsx +120 -0
- package/web/components/editor/EditorTabs.tsx +229 -0
- package/web/components/editor/MarkdownPreview.tsx +154 -0
- package/web/components/editor/MermaidDiagram.tsx +113 -0
- package/web/components/editor/MonacoEditor.tsx +177 -0
- package/web/components/editor/TabContextMenu.tsx +207 -0
- package/web/components/git/GitPanel.tsx +534 -0
- package/web/components/layout/Shell.tsx +15 -0
- package/web/components/layout/StatusBar.tsx +100 -0
- package/web/components/modals/CommandPalette.tsx +393 -0
- package/web/components/modals/GlobalSearch.tsx +348 -0
- package/web/components/modals/QuickOpen.tsx +241 -0
- package/web/components/modals/RecentFiles.tsx +208 -0
- package/web/components/projects/ProjectSelector.tsx +147 -0
- package/web/components/settings/SettingItem.tsx +150 -0
- package/web/components/settings/SettingsPanel.tsx +323 -0
- package/web/components/specs/SpecsPanel.tsx +1091 -0
- package/web/components/terminal/TerminalPanel.tsx +683 -0
- package/web/components/ui/ContextMenu.tsx +182 -0
- package/web/components/ui/LoadingSpinner.tsx +66 -0
- package/web/components/ui/ResizeHandle.tsx +110 -0
- package/web/components/ui/Skeleton.tsx +108 -0
- package/web/components/ui/SkipLinks.tsx +37 -0
- package/web/components/ui/Toaster.tsx +57 -0
- package/web/hooks/useFocusTrap.ts +141 -0
- package/web/hooks/useKeyboardShortcuts.ts +169 -0
- package/web/hooks/useListNavigation.ts +237 -0
- package/web/lib/autopilotConstants.ts +213 -0
- package/web/lib/constants/agents.ts +67 -0
- package/web/lib/git.ts +339 -0
- package/web/lib/ptyManager.ts +191 -0
- package/web/lib/specsParser.ts +299 -0
- package/web/lib/stores/autopilotStore.ts +288 -0
- package/web/lib/stores/fileStore.ts +550 -0
- package/web/lib/stores/gitStore.ts +386 -0
- package/web/lib/stores/projectStore.ts +196 -0
- package/web/lib/stores/settingsStore.ts +126 -0
- package/web/lib/stores/specsStore.ts +297 -0
- package/web/lib/stores/uiStore.ts +175 -0
- package/web/lib/types/index.ts +177 -0
- package/web/lib/utils.ts +98 -0
- package/web/next.config.js +50 -0
- package/web/package.json +54 -0
- package/web/postcss.config.js +6 -0
- package/web/tailwind.config.ts +68 -0
- package/web/tsconfig.json +41 -0
package/web/lib/git.ts
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import simpleGit, { SimpleGit, StatusResult, DiffResult, LogResult } from 'simple-git';
|
|
2
|
+
|
|
3
|
+
export interface GitStatus {
|
|
4
|
+
isRepo: boolean;
|
|
5
|
+
branch: string;
|
|
6
|
+
ahead: number;
|
|
7
|
+
behind: number;
|
|
8
|
+
staged: GitFileChange[];
|
|
9
|
+
unstaged: GitFileChange[];
|
|
10
|
+
untracked: string[];
|
|
11
|
+
conflicted: string[];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface GitFileChange {
|
|
15
|
+
path: string;
|
|
16
|
+
status: 'added' | 'modified' | 'deleted' | 'renamed' | 'copied';
|
|
17
|
+
oldPath?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface GitCommit {
|
|
21
|
+
hash: string;
|
|
22
|
+
hashShort: string;
|
|
23
|
+
author: string;
|
|
24
|
+
email: string;
|
|
25
|
+
date: string;
|
|
26
|
+
message: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GitBranch {
|
|
30
|
+
name: string;
|
|
31
|
+
current: boolean;
|
|
32
|
+
commit: string;
|
|
33
|
+
label: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface GitDiff {
|
|
37
|
+
file: string;
|
|
38
|
+
additions: number;
|
|
39
|
+
deletions: number;
|
|
40
|
+
chunks: GitDiffChunk[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface GitDiffChunk {
|
|
44
|
+
header: string;
|
|
45
|
+
lines: GitDiffLine[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface GitDiffLine {
|
|
49
|
+
type: 'add' | 'delete' | 'context';
|
|
50
|
+
content: string;
|
|
51
|
+
oldLineNo?: number;
|
|
52
|
+
newLineNo?: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function mapFileStatus(index: string, workingDir: string): 'added' | 'modified' | 'deleted' | 'renamed' | 'copied' {
|
|
56
|
+
const status = index !== ' ' ? index : workingDir;
|
|
57
|
+
switch (status) {
|
|
58
|
+
case 'A': return 'added';
|
|
59
|
+
case 'M': return 'modified';
|
|
60
|
+
case 'D': return 'deleted';
|
|
61
|
+
case 'R': return 'renamed';
|
|
62
|
+
case 'C': return 'copied';
|
|
63
|
+
default: return 'modified';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function getGitStatus(cwd: string): Promise<GitStatus> {
|
|
68
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const isRepo = await git.checkIsRepo();
|
|
72
|
+
if (!isRepo) {
|
|
73
|
+
return {
|
|
74
|
+
isRepo: false,
|
|
75
|
+
branch: '',
|
|
76
|
+
ahead: 0,
|
|
77
|
+
behind: 0,
|
|
78
|
+
staged: [],
|
|
79
|
+
unstaged: [],
|
|
80
|
+
untracked: [],
|
|
81
|
+
conflicted: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const status: StatusResult = await git.status();
|
|
86
|
+
|
|
87
|
+
const staged: GitFileChange[] = [];
|
|
88
|
+
const unstaged: GitFileChange[] = [];
|
|
89
|
+
|
|
90
|
+
// Process files
|
|
91
|
+
status.files.forEach((file) => {
|
|
92
|
+
// Staged changes (index)
|
|
93
|
+
if (file.index !== ' ' && file.index !== '?') {
|
|
94
|
+
staged.push({
|
|
95
|
+
path: file.path,
|
|
96
|
+
status: mapFileStatus(file.index, ' '),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Unstaged changes (working directory)
|
|
101
|
+
if (file.working_dir !== ' ' && file.working_dir !== '?') {
|
|
102
|
+
unstaged.push({
|
|
103
|
+
path: file.path,
|
|
104
|
+
status: mapFileStatus(' ', file.working_dir),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
isRepo: true,
|
|
111
|
+
branch: status.current || 'HEAD',
|
|
112
|
+
ahead: status.ahead,
|
|
113
|
+
behind: status.behind,
|
|
114
|
+
staged,
|
|
115
|
+
unstaged,
|
|
116
|
+
untracked: status.not_added,
|
|
117
|
+
conflicted: status.conflicted,
|
|
118
|
+
};
|
|
119
|
+
} catch (error) {
|
|
120
|
+
console.error('Git status error:', error);
|
|
121
|
+
return {
|
|
122
|
+
isRepo: false,
|
|
123
|
+
branch: '',
|
|
124
|
+
ahead: 0,
|
|
125
|
+
behind: 0,
|
|
126
|
+
staged: [],
|
|
127
|
+
unstaged: [],
|
|
128
|
+
untracked: [],
|
|
129
|
+
conflicted: [],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export async function getGitLog(cwd: string, maxCount: number = 50): Promise<GitCommit[]> {
|
|
135
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
const log: LogResult = await git.log({ maxCount });
|
|
139
|
+
|
|
140
|
+
return log.all.map((commit) => ({
|
|
141
|
+
hash: commit.hash,
|
|
142
|
+
hashShort: commit.hash.substring(0, 7),
|
|
143
|
+
author: commit.author_name,
|
|
144
|
+
email: commit.author_email,
|
|
145
|
+
date: commit.date,
|
|
146
|
+
message: commit.message,
|
|
147
|
+
}));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('Git log error:', error);
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export async function getGitBranches(cwd: string): Promise<GitBranch[]> {
|
|
155
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
const branches = await git.branchLocal();
|
|
159
|
+
|
|
160
|
+
return branches.all.map((name) => ({
|
|
161
|
+
name,
|
|
162
|
+
current: name === branches.current,
|
|
163
|
+
commit: branches.branches[name]?.commit || '',
|
|
164
|
+
label: branches.branches[name]?.label || name,
|
|
165
|
+
}));
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error('Git branches error:', error);
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export async function getGitDiff(cwd: string, file?: string, staged: boolean = false): Promise<string> {
|
|
173
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
const args = staged ? ['--cached'] : [];
|
|
177
|
+
if (file) {
|
|
178
|
+
args.push('--', file);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const diff = await git.diff(args);
|
|
182
|
+
return diff;
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.error('Git diff error:', error);
|
|
185
|
+
return '';
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export async function gitStage(cwd: string, files: string[]): Promise<boolean> {
|
|
190
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
191
|
+
|
|
192
|
+
try {
|
|
193
|
+
await git.add(files);
|
|
194
|
+
return true;
|
|
195
|
+
} catch (error) {
|
|
196
|
+
console.error('Git stage error:', error);
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export async function gitUnstage(cwd: string, files: string[]): Promise<boolean> {
|
|
202
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
await git.reset(['HEAD', '--', ...files]);
|
|
206
|
+
return true;
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error('Git unstage error:', error);
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
export async function gitCommit(cwd: string, message: string): Promise<{ success: boolean; hash?: string; error?: string }> {
|
|
214
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
215
|
+
|
|
216
|
+
try {
|
|
217
|
+
const result = await git.commit(message);
|
|
218
|
+
return {
|
|
219
|
+
success: true,
|
|
220
|
+
hash: result.commit,
|
|
221
|
+
};
|
|
222
|
+
} catch (error) {
|
|
223
|
+
console.error('Git commit error:', error);
|
|
224
|
+
return {
|
|
225
|
+
success: false,
|
|
226
|
+
error: error instanceof Error ? error.message : 'Commit failed',
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export async function gitPush(cwd: string, remote: string = 'origin', branch?: string): Promise<{ success: boolean; error?: string }> {
|
|
232
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const status = await git.status();
|
|
236
|
+
const targetBranch = branch || status.current || 'main';
|
|
237
|
+
await git.push(remote, targetBranch);
|
|
238
|
+
return { success: true };
|
|
239
|
+
} catch (error) {
|
|
240
|
+
console.error('Git push error:', error);
|
|
241
|
+
return {
|
|
242
|
+
success: false,
|
|
243
|
+
error: error instanceof Error ? error.message : 'Push failed',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export async function gitPull(cwd: string, remote: string = 'origin', branch?: string): Promise<{ success: boolean; error?: string }> {
|
|
249
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
250
|
+
|
|
251
|
+
try {
|
|
252
|
+
const status = await git.status();
|
|
253
|
+
const targetBranch = branch || status.current || 'main';
|
|
254
|
+
await git.pull(remote, targetBranch);
|
|
255
|
+
return { success: true };
|
|
256
|
+
} catch (error) {
|
|
257
|
+
console.error('Git pull error:', error);
|
|
258
|
+
return {
|
|
259
|
+
success: false,
|
|
260
|
+
error: error instanceof Error ? error.message : 'Pull failed',
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function gitCheckout(cwd: string, branch: string): Promise<{ success: boolean; error?: string }> {
|
|
266
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
267
|
+
|
|
268
|
+
try {
|
|
269
|
+
await git.checkout(branch);
|
|
270
|
+
return { success: true };
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.error('Git checkout error:', error);
|
|
273
|
+
return {
|
|
274
|
+
success: false,
|
|
275
|
+
error: error instanceof Error ? error.message : 'Checkout failed',
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export async function gitCreateBranch(cwd: string, branch: string, checkout: boolean = true): Promise<{ success: boolean; error?: string }> {
|
|
281
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
if (checkout) {
|
|
285
|
+
await git.checkoutLocalBranch(branch);
|
|
286
|
+
} else {
|
|
287
|
+
await git.branch([branch]);
|
|
288
|
+
}
|
|
289
|
+
return { success: true };
|
|
290
|
+
} catch (error) {
|
|
291
|
+
console.error('Git create branch error:', error);
|
|
292
|
+
return {
|
|
293
|
+
success: false,
|
|
294
|
+
error: error instanceof Error ? error.message : 'Create branch failed',
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
export async function gitDiscard(cwd: string, files: string[]): Promise<boolean> {
|
|
300
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
301
|
+
|
|
302
|
+
try {
|
|
303
|
+
await git.checkout(['--', ...files]);
|
|
304
|
+
return true;
|
|
305
|
+
} catch (error) {
|
|
306
|
+
console.error('Git discard error:', error);
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export async function gitInit(cwd: string): Promise<{ success: boolean; error?: string }> {
|
|
312
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
await git.init();
|
|
316
|
+
return { success: true };
|
|
317
|
+
} catch (error) {
|
|
318
|
+
console.error('Git init error:', error);
|
|
319
|
+
return {
|
|
320
|
+
success: false,
|
|
321
|
+
error: error instanceof Error ? error.message : 'Init failed',
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export async function getRemotes(cwd: string): Promise<{ name: string; url: string }[]> {
|
|
327
|
+
const git: SimpleGit = simpleGit(cwd);
|
|
328
|
+
|
|
329
|
+
try {
|
|
330
|
+
const remotes = await git.getRemotes(true);
|
|
331
|
+
return remotes.map((r) => ({
|
|
332
|
+
name: r.name,
|
|
333
|
+
url: r.refs.fetch || r.refs.push || '',
|
|
334
|
+
}));
|
|
335
|
+
} catch (error) {
|
|
336
|
+
console.error('Git remotes error:', error);
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import * as pty from 'node-pty';
|
|
2
|
+
import { EventEmitter } from 'events';
|
|
3
|
+
import { PHASE_DONE_REGEX } from '@/lib/autopilotConstants';
|
|
4
|
+
|
|
5
|
+
interface TerminalSession {
|
|
6
|
+
id: string;
|
|
7
|
+
pty: pty.IPty;
|
|
8
|
+
cwd: string;
|
|
9
|
+
createdAt: Date;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
interface AutopilotCollector {
|
|
13
|
+
buffer: string;
|
|
14
|
+
resolve: (result: { output: string; exitCode: number }) => void;
|
|
15
|
+
reject: (error: Error) => void;
|
|
16
|
+
timeout: NodeJS.Timeout;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
class PtyManager extends EventEmitter {
|
|
20
|
+
private sessions: Map<string, TerminalSession> = new Map();
|
|
21
|
+
private outputBuffers: Map<string, string[]> = new Map();
|
|
22
|
+
private autopilotCollectors: Map<string, AutopilotCollector> = new Map();
|
|
23
|
+
|
|
24
|
+
createSession(id: string, cwd: string, cols: number = 80, rows: number = 24): TerminalSession {
|
|
25
|
+
// Determine shell based on platform
|
|
26
|
+
const shell = process.platform === 'win32' ? 'powershell.exe' : process.env.SHELL || '/bin/zsh';
|
|
27
|
+
const shellArgs = process.platform === 'win32' ? [] : ['-l'];
|
|
28
|
+
|
|
29
|
+
const ptyProcess = pty.spawn(shell, shellArgs, {
|
|
30
|
+
name: 'xterm-256color',
|
|
31
|
+
cols,
|
|
32
|
+
rows,
|
|
33
|
+
cwd,
|
|
34
|
+
env: {
|
|
35
|
+
...process.env,
|
|
36
|
+
TERM: 'xterm-256color',
|
|
37
|
+
COLORTERM: 'truecolor',
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const session: TerminalSession = {
|
|
42
|
+
id,
|
|
43
|
+
pty: ptyProcess,
|
|
44
|
+
cwd,
|
|
45
|
+
createdAt: new Date(),
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
this.sessions.set(id, session);
|
|
49
|
+
this.outputBuffers.set(id, []);
|
|
50
|
+
|
|
51
|
+
// Handle data from PTY
|
|
52
|
+
ptyProcess.onData((data) => {
|
|
53
|
+
const buffer = this.outputBuffers.get(id);
|
|
54
|
+
if (buffer) {
|
|
55
|
+
buffer.push(data);
|
|
56
|
+
// Keep buffer size manageable
|
|
57
|
+
if (buffer.length > 1000) {
|
|
58
|
+
buffer.shift();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Check autopilot collector for completion marker
|
|
63
|
+
const collector = this.autopilotCollectors.get(id);
|
|
64
|
+
if (collector) {
|
|
65
|
+
collector.buffer += data;
|
|
66
|
+
const markerMatch = collector.buffer.match(PHASE_DONE_REGEX);
|
|
67
|
+
if (markerMatch) {
|
|
68
|
+
const exitCode = parseInt(markerMatch[1], 10);
|
|
69
|
+
// Extract output before the marker
|
|
70
|
+
const output = collector.buffer.split(PHASE_DONE_REGEX)[0];
|
|
71
|
+
clearTimeout(collector.timeout);
|
|
72
|
+
this.autopilotCollectors.delete(id);
|
|
73
|
+
this.emit('autopilot-phase-done', { sessionId: id, output, exitCode });
|
|
74
|
+
collector.resolve({ output, exitCode });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
this.emit('data', { sessionId: id, data });
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Handle PTY exit
|
|
82
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
83
|
+
// If there's an active collector, reject it
|
|
84
|
+
const collector = this.autopilotCollectors.get(id);
|
|
85
|
+
if (collector) {
|
|
86
|
+
clearTimeout(collector.timeout);
|
|
87
|
+
this.autopilotCollectors.delete(id);
|
|
88
|
+
collector.reject(new Error(`Terminal exited with code ${exitCode} during autopilot phase`));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.emit('exit', { sessionId: id, exitCode });
|
|
92
|
+
this.sessions.delete(id);
|
|
93
|
+
this.outputBuffers.delete(id);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return session;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
getSession(id: string): TerminalSession | undefined {
|
|
100
|
+
return this.sessions.get(id);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
write(id: string, data: string): boolean {
|
|
104
|
+
const session = this.sessions.get(id);
|
|
105
|
+
if (session) {
|
|
106
|
+
session.pty.write(data);
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
resize(id: string, cols: number, rows: number): boolean {
|
|
113
|
+
const session = this.sessions.get(id);
|
|
114
|
+
if (session) {
|
|
115
|
+
session.pty.resize(cols, rows);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
destroySession(id: string): boolean {
|
|
122
|
+
const session = this.sessions.get(id);
|
|
123
|
+
if (session) {
|
|
124
|
+
// Clean up any active collector
|
|
125
|
+
const collector = this.autopilotCollectors.get(id);
|
|
126
|
+
if (collector) {
|
|
127
|
+
clearTimeout(collector.timeout);
|
|
128
|
+
this.autopilotCollectors.delete(id);
|
|
129
|
+
collector.reject(new Error('Session destroyed'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
session.pty.kill();
|
|
133
|
+
this.sessions.delete(id);
|
|
134
|
+
this.outputBuffers.delete(id);
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
getOutputBuffer(id: string): string[] {
|
|
141
|
+
return this.outputBuffers.get(id) || [];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
clearOutputBuffer(id: string): void {
|
|
145
|
+
const buffer = this.outputBuffers.get(id);
|
|
146
|
+
if (buffer) {
|
|
147
|
+
buffer.length = 0;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
getActiveSessions(): string[] {
|
|
152
|
+
return Array.from(this.sessions.keys());
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Arm an autopilot collector for a terminal session.
|
|
157
|
+
* Returns a Promise that resolves when the completion marker is detected in the output.
|
|
158
|
+
*/
|
|
159
|
+
armAutopilotCollector(sessionId: string, timeoutMs: number): Promise<{ output: string; exitCode: number }> {
|
|
160
|
+
// Disarm any existing collector
|
|
161
|
+
this.disarmAutopilotCollector(sessionId);
|
|
162
|
+
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const timeout = setTimeout(() => {
|
|
165
|
+
this.autopilotCollectors.delete(sessionId);
|
|
166
|
+
reject(new Error(`Autopilot phase timed out after ${Math.round(timeoutMs / 1000)}s`));
|
|
167
|
+
}, timeoutMs);
|
|
168
|
+
|
|
169
|
+
this.autopilotCollectors.set(sessionId, {
|
|
170
|
+
buffer: '',
|
|
171
|
+
resolve,
|
|
172
|
+
reject,
|
|
173
|
+
timeout,
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Disarm (cancel) an active autopilot collector for a session.
|
|
180
|
+
*/
|
|
181
|
+
disarmAutopilotCollector(sessionId: string): void {
|
|
182
|
+
const collector = this.autopilotCollectors.get(sessionId);
|
|
183
|
+
if (collector) {
|
|
184
|
+
clearTimeout(collector.timeout);
|
|
185
|
+
this.autopilotCollectors.delete(sessionId);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Singleton instance
|
|
191
|
+
export const ptyManager = new PtyManager();
|