@lovelybunch/api 1.0.7
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/lib/gait-path.d.ts +13 -0
- package/dist/lib/gait-path.js +57 -0
- package/dist/lib/project-paths.d.ts +13 -0
- package/dist/lib/project-paths.js +57 -0
- package/dist/lib/storage/file-storage.d.ts +28 -0
- package/dist/lib/storage/file-storage.js +224 -0
- package/dist/lib/symlinks/symlink-manager.d.ts +66 -0
- package/dist/lib/symlinks/symlink-manager.js +444 -0
- package/dist/lib/symlinks/types.d.ts +23 -0
- package/dist/lib/symlinks/types.js +4 -0
- package/dist/lib/terminal/context-helper.d.ts +11 -0
- package/dist/lib/terminal/context-helper.js +164 -0
- package/dist/lib/terminal/global-manager.d.ts +2 -0
- package/dist/lib/terminal/global-manager.js +15 -0
- package/dist/lib/terminal/shell-utils.d.ts +33 -0
- package/dist/lib/terminal/shell-utils.js +176 -0
- package/dist/lib/terminal/terminal-manager.d.ts +26 -0
- package/dist/lib/terminal/terminal-manager.js +276 -0
- package/dist/lib/user-preferences.d.ts +48 -0
- package/dist/lib/user-preferences.js +87 -0
- package/dist/lib/utils.d.ts +2 -0
- package/dist/lib/utils.js +5 -0
- package/dist/routes/api/symlink-status/route.d.ts +1 -0
- package/dist/routes/api/symlink-status/route.js +37 -0
- package/dist/routes/api/symlinks/[id]/route.d.ts +19 -0
- package/dist/routes/api/symlinks/[id]/route.js +95 -0
- package/dist/routes/api/symlinks/[id]/toggle/route.d.ts +11 -0
- package/dist/routes/api/symlinks/[id]/toggle/route.js +32 -0
- package/dist/routes/api/symlinks/debug/route.d.ts +1 -0
- package/dist/routes/api/symlinks/debug/route.js +35 -0
- package/dist/routes/api/symlinks/route.d.ts +9 -0
- package/dist/routes/api/symlinks/route.js +72 -0
- package/dist/routes/api/toggle-symlink/route.d.ts +2 -0
- package/dist/routes/api/toggle-symlink/route.js +94 -0
- package/dist/routes/api/v1/agents/[id]/index.d.ts +1 -0
- package/dist/routes/api/v1/agents/[id]/index.js +1 -0
- package/dist/routes/api/v1/agents/[id]/route.d.ts +3 -0
- package/dist/routes/api/v1/agents/[id]/route.js +163 -0
- package/dist/routes/api/v1/agents/index.d.ts +1 -0
- package/dist/routes/api/v1/agents/index.js +1 -0
- package/dist/routes/api/v1/agents/route.d.ts +3 -0
- package/dist/routes/api/v1/agents/route.js +133 -0
- package/dist/routes/api/v1/ai/index.d.ts +3 -0
- package/dist/routes/api/v1/ai/index.js +5 -0
- package/dist/routes/api/v1/ai/route.d.ts +8 -0
- package/dist/routes/api/v1/ai/route.js +86 -0
- package/dist/routes/api/v1/chats/[id]/index.d.ts +3 -0
- package/dist/routes/api/v1/chats/[id]/index.js +6 -0
- package/dist/routes/api/v1/chats/[id]/route.d.ts +12 -0
- package/dist/routes/api/v1/chats/[id]/route.js +31 -0
- package/dist/routes/api/v1/chats/index.d.ts +3 -0
- package/dist/routes/api/v1/chats/index.js +6 -0
- package/dist/routes/api/v1/chats/route.d.ts +32 -0
- package/dist/routes/api/v1/chats/route.js +67 -0
- package/dist/routes/api/v1/config/index.d.ts +3 -0
- package/dist/routes/api/v1/config/index.js +5 -0
- package/dist/routes/api/v1/config/route.d.ts +9 -0
- package/dist/routes/api/v1/config/route.js +29 -0
- package/dist/routes/api/v1/context/[...path]/route.d.ts +16 -0
- package/dist/routes/api/v1/context/[...path]/route.js +107 -0
- package/dist/routes/api/v1/context/architecture/route.d.ts +3 -0
- package/dist/routes/api/v1/context/architecture/route.js +198 -0
- package/dist/routes/api/v1/context/index.d.ts +3 -0
- package/dist/routes/api/v1/context/index.js +9 -0
- package/dist/routes/api/v1/context/knowledge/[filename]/index.d.ts +1 -0
- package/dist/routes/api/v1/context/knowledge/[filename]/index.js +1 -0
- package/dist/routes/api/v1/context/knowledge/[filename]/route.d.ts +3 -0
- package/dist/routes/api/v1/context/knowledge/[filename]/route.js +165 -0
- package/dist/routes/api/v1/context/knowledge/index.d.ts +1 -0
- package/dist/routes/api/v1/context/knowledge/index.js +1 -0
- package/dist/routes/api/v1/context/knowledge/route.d.ts +3 -0
- package/dist/routes/api/v1/context/knowledge/route.js +121 -0
- package/dist/routes/api/v1/context/project/route.d.ts +3 -0
- package/dist/routes/api/v1/context/project/route.js +153 -0
- package/dist/routes/api/v1/proposals/[id]/route.d.ts +337 -0
- package/dist/routes/api/v1/proposals/[id]/route.js +99 -0
- package/dist/routes/api/v1/proposals/index.d.ts +3 -0
- package/dist/routes/api/v1/proposals/index.js +10 -0
- package/dist/routes/api/v1/proposals/route.d.ts +315 -0
- package/dist/routes/api/v1/proposals/route.js +103 -0
- package/dist/routes/api/v1/resources/[id]/index.d.ts +3 -0
- package/dist/routes/api/v1/resources/[id]/index.js +7 -0
- package/dist/routes/api/v1/resources/[id]/route.d.ts +46 -0
- package/dist/routes/api/v1/resources/[id]/route.js +143 -0
- package/dist/routes/api/v1/resources/[id]/thumbnail/index.d.ts +3 -0
- package/dist/routes/api/v1/resources/[id]/thumbnail/index.js +5 -0
- package/dist/routes/api/v1/resources/[id]/thumbnail/route.d.ts +2 -0
- package/dist/routes/api/v1/resources/[id]/thumbnail/route.js +50 -0
- package/dist/routes/api/v1/resources/index.d.ts +3 -0
- package/dist/routes/api/v1/resources/index.js +6 -0
- package/dist/routes/api/v1/resources/route.d.ts +51 -0
- package/dist/routes/api/v1/resources/route.js +147 -0
- package/dist/routes/api/v1/search/route.d.ts +3 -0
- package/dist/routes/api/v1/search/route.js +39 -0
- package/dist/routes/api/v1/terminal/[proposalId]/create/index.d.ts +3 -0
- package/dist/routes/api/v1/terminal/[proposalId]/create/index.js +5 -0
- package/dist/routes/api/v1/terminal/[proposalId]/create/route.d.ts +10 -0
- package/dist/routes/api/v1/terminal/[proposalId]/create/route.js +27 -0
- package/dist/routes/api/v1/terminal/[proposalId]/destroy/index.d.ts +3 -0
- package/dist/routes/api/v1/terminal/[proposalId]/destroy/index.js +5 -0
- package/dist/routes/api/v1/terminal/[proposalId]/destroy/route.d.ts +10 -0
- package/dist/routes/api/v1/terminal/[proposalId]/destroy/route.js +21 -0
- package/dist/routes/api/v1/terminal/[proposalId]/resize/index.d.ts +3 -0
- package/dist/routes/api/v1/terminal/[proposalId]/resize/index.js +5 -0
- package/dist/routes/api/v1/terminal/[proposalId]/resize/route.d.ts +10 -0
- package/dist/routes/api/v1/terminal/[proposalId]/resize/route.js +21 -0
- package/dist/routes/api/v1/terminal/sessions/index.d.ts +3 -0
- package/dist/routes/api/v1/terminal/sessions/index.js +5 -0
- package/dist/routes/api/v1/terminal/sessions/route.d.ts +6 -0
- package/dist/routes/api/v1/terminal/sessions/route.js +29 -0
- package/dist/routes/api/v1/user/index.d.ts +3 -0
- package/dist/routes/api/v1/user/index.js +5 -0
- package/dist/routes/api/v1/user/preferences/route.d.ts +11 -0
- package/dist/routes/api/v1/user/preferences/route.js +31 -0
- package/dist/routes/api/v1/user/profile/route.d.ts +11 -0
- package/dist/routes/api/v1/user/profile/route.js +31 -0
- package/dist/routes/api/v1/user/settings/index.d.ts +1 -0
- package/dist/routes/api/v1/user/settings/index.js +1 -0
- package/dist/routes/api/v1/user/settings/route.d.ts +3 -0
- package/dist/routes/api/v1/user/settings/route.js +51 -0
- package/dist/server-with-static.d.ts +4 -0
- package/dist/server-with-static.js +144 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +91 -0
- package/package.json +42 -0
- package/static/assets/index-BvTnrm0O.js +576 -0
- package/static/assets/index-Cm5dZHTl.css +33 -0
- package/static/assets/index-ORkAkJNi.js +576 -0
- package/static/assets/index-_Keadpms.js +576 -0
- package/static/index.html +17 -0
- package/static/vite.svg +1 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type ShellType = 'bash' | 'zsh' | 'fish' | 'sh' | 'system';
|
|
2
|
+
export interface ShellInfo {
|
|
3
|
+
name: ShellType;
|
|
4
|
+
displayName: string;
|
|
5
|
+
path: string;
|
|
6
|
+
available: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Get the default system shell
|
|
10
|
+
*/
|
|
11
|
+
export declare function getSystemShell(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Get information about available shells on the system
|
|
14
|
+
*/
|
|
15
|
+
export declare function getAvailableShells(): Promise<ShellInfo[]>;
|
|
16
|
+
/**
|
|
17
|
+
* Get the shell executable path based on user preference
|
|
18
|
+
*/
|
|
19
|
+
export declare function getShellPath(preference?: ShellType): Promise<string>;
|
|
20
|
+
/**
|
|
21
|
+
* Get shell arguments for spawning a new shell with an init script
|
|
22
|
+
*/
|
|
23
|
+
export declare function getShellArgs(shellPath: string, initScriptPath: string): string[];
|
|
24
|
+
/**
|
|
25
|
+
* Prepare shell-specific initialization
|
|
26
|
+
*/
|
|
27
|
+
export declare function prepareShellInit(shellPath: string, initScript: string): {
|
|
28
|
+
script: string;
|
|
29
|
+
needsEnvVar?: {
|
|
30
|
+
name: string;
|
|
31
|
+
value: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
/**
|
|
4
|
+
* Get the default system shell
|
|
5
|
+
*/
|
|
6
|
+
export function getSystemShell() {
|
|
7
|
+
// Get the user's default shell from environment or passwd
|
|
8
|
+
const userShell = process.env.SHELL || '/bin/bash';
|
|
9
|
+
return userShell;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Check if a shell executable exists and is accessible
|
|
13
|
+
*/
|
|
14
|
+
async function isShellAvailable(shellPath) {
|
|
15
|
+
try {
|
|
16
|
+
await fs.access(shellPath, fs.constants.X_OK);
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Get information about available shells on the system
|
|
25
|
+
*/
|
|
26
|
+
export async function getAvailableShells() {
|
|
27
|
+
const shells = [
|
|
28
|
+
{
|
|
29
|
+
name: 'bash',
|
|
30
|
+
displayName: 'Bash',
|
|
31
|
+
path: '/bin/bash',
|
|
32
|
+
available: false,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'zsh',
|
|
36
|
+
displayName: 'Zsh',
|
|
37
|
+
path: '/bin/zsh',
|
|
38
|
+
available: false,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'fish',
|
|
42
|
+
displayName: 'Fish',
|
|
43
|
+
path: '/usr/local/bin/fish',
|
|
44
|
+
available: false,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: 'sh',
|
|
48
|
+
displayName: 'Sh (Bourne Shell)',
|
|
49
|
+
path: '/bin/sh',
|
|
50
|
+
available: false,
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'system',
|
|
54
|
+
displayName: 'System Default',
|
|
55
|
+
path: getSystemShell(),
|
|
56
|
+
available: true,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
// Check availability of each shell
|
|
60
|
+
for (const shell of shells) {
|
|
61
|
+
if (shell.name !== 'system') {
|
|
62
|
+
shell.available = await isShellAvailable(shell.path);
|
|
63
|
+
// For fish, also check alternative location
|
|
64
|
+
if (shell.name === 'fish' && !shell.available) {
|
|
65
|
+
const altPath = '/opt/homebrew/bin/fish';
|
|
66
|
+
if (await isShellAvailable(altPath)) {
|
|
67
|
+
shell.path = altPath;
|
|
68
|
+
shell.available = true;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
// For zsh on macOS, check alternative locations
|
|
72
|
+
if (shell.name === 'zsh' && !shell.available && os.platform() === 'darwin') {
|
|
73
|
+
const altPaths = ['/usr/local/bin/zsh', '/opt/homebrew/bin/zsh'];
|
|
74
|
+
for (const altPath of altPaths) {
|
|
75
|
+
if (await isShellAvailable(altPath)) {
|
|
76
|
+
shell.path = altPath;
|
|
77
|
+
shell.available = true;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return shells;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Get the shell executable path based on user preference
|
|
88
|
+
*/
|
|
89
|
+
export async function getShellPath(preference = 'bash') {
|
|
90
|
+
if (preference === 'system') {
|
|
91
|
+
return getSystemShell();
|
|
92
|
+
}
|
|
93
|
+
const shells = await getAvailableShells();
|
|
94
|
+
const selectedShell = shells.find(s => s.name === preference);
|
|
95
|
+
if (selectedShell && selectedShell.available) {
|
|
96
|
+
return selectedShell.path;
|
|
97
|
+
}
|
|
98
|
+
// Fallback to bash if preferred shell is not available
|
|
99
|
+
const bashShell = shells.find(s => s.name === 'bash');
|
|
100
|
+
if (bashShell && bashShell.available) {
|
|
101
|
+
return bashShell.path;
|
|
102
|
+
}
|
|
103
|
+
// Ultimate fallback to system shell
|
|
104
|
+
return getSystemShell();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get shell arguments for spawning a new shell with an init script
|
|
108
|
+
*/
|
|
109
|
+
export function getShellArgs(shellPath, initScriptPath) {
|
|
110
|
+
const shellName = shellPath.split('/').pop() || 'bash';
|
|
111
|
+
switch (shellName) {
|
|
112
|
+
case 'bash':
|
|
113
|
+
return ['--init-file', initScriptPath];
|
|
114
|
+
case 'zsh':
|
|
115
|
+
// For zsh, we need to use ZDOTDIR environment variable
|
|
116
|
+
// The init script will need to be named .zshrc in a temp directory
|
|
117
|
+
return ['-i'];
|
|
118
|
+
case 'fish':
|
|
119
|
+
// Fish uses --init-command
|
|
120
|
+
return ['--init-command', `source ${initScriptPath}`];
|
|
121
|
+
case 'sh':
|
|
122
|
+
// Basic sh doesn't support init files the same way
|
|
123
|
+
// We'll need to source it manually after start
|
|
124
|
+
return [];
|
|
125
|
+
default:
|
|
126
|
+
// Try bash-style for unknown shells
|
|
127
|
+
return ['--init-file', initScriptPath];
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Prepare shell-specific initialization
|
|
132
|
+
*/
|
|
133
|
+
export function prepareShellInit(shellPath, initScript) {
|
|
134
|
+
const shellName = shellPath.split('/').pop() || 'bash';
|
|
135
|
+
switch (shellName) {
|
|
136
|
+
case 'zsh':
|
|
137
|
+
// For zsh, we need to create a proper .zshrc that sources the system one first
|
|
138
|
+
// Use proper zsh syntax and prevent content echo
|
|
139
|
+
// Also fix the prompt to use zsh syntax
|
|
140
|
+
const zshScript = initScript
|
|
141
|
+
// Replace bash-style prompt with zsh-style prompt
|
|
142
|
+
.replace(/export PS1=.*$/m, 'export PROMPT="%F{cyan}[${GAIT_PROPOSAL_ID}]%f %~ %# "');
|
|
143
|
+
return {
|
|
144
|
+
script: `#!/bin/zsh
|
|
145
|
+
# GAIT Zsh Initialization
|
|
146
|
+
|
|
147
|
+
# Source system zshrc if it exists (suppress any output)
|
|
148
|
+
[ -f ~/.zshrc ] && source ~/.zshrc > /dev/null 2>&1 || true
|
|
149
|
+
|
|
150
|
+
# Custom GAIT initialization
|
|
151
|
+
${zshScript}`,
|
|
152
|
+
needsEnvVar: { name: 'ZDOTDIR', value: '' } // Will be set to temp dir
|
|
153
|
+
};
|
|
154
|
+
case 'fish':
|
|
155
|
+
// Fish script syntax is different
|
|
156
|
+
const fishScript = initScript
|
|
157
|
+
.replace(/export\\s+(\\w+)=(.*)/g, 'set -gx $1 $2')
|
|
158
|
+
.replace(/alias\\s+(\\w+)=(.*)/g, 'alias $1 $2')
|
|
159
|
+
// Fish prompt syntax
|
|
160
|
+
.replace(/export PS1=.*$/m, 'function fish_prompt\n echo -n (set_color cyan)"[$GAIT_PROPOSAL_ID]"(set_color normal)" "(prompt_pwd)" > "\nend');
|
|
161
|
+
return {
|
|
162
|
+
script: fishScript
|
|
163
|
+
};
|
|
164
|
+
case 'bash':
|
|
165
|
+
default:
|
|
166
|
+
// Bash uses the script with proper PS1 escaping
|
|
167
|
+
const bashScript = initScript.replace(/export PS1="\[(.*)\] \\w \$ "/, 'export PS1="\\[\\033[36m\\][$GAIT_PROPOSAL_ID]\\[\\033[0m\\] \\w \\$ "');
|
|
168
|
+
return {
|
|
169
|
+
script: `# Source system bashrc if it exists
|
|
170
|
+
[ -f ~/.bashrc ] && source ~/.bashrc
|
|
171
|
+
|
|
172
|
+
# Custom GAIT initialization
|
|
173
|
+
${bashScript}`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
import { ShellType } from './shell-utils.js';
|
|
3
|
+
export interface TerminalSession {
|
|
4
|
+
id: string;
|
|
5
|
+
proposalId: string;
|
|
6
|
+
pty: any;
|
|
7
|
+
websocket?: WebSocket;
|
|
8
|
+
createdAt: Date;
|
|
9
|
+
lastActivity: Date;
|
|
10
|
+
enableLogging?: boolean;
|
|
11
|
+
logFilePath?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare class TerminalManager {
|
|
14
|
+
private sessions;
|
|
15
|
+
private cleanupInterval;
|
|
16
|
+
constructor();
|
|
17
|
+
createSession(proposalId: string, enableLogging?: boolean, shellPreference?: ShellType): Promise<TerminalSession>;
|
|
18
|
+
getSession(sessionId: string): TerminalSession | undefined;
|
|
19
|
+
getSessionsByProposal(proposalId: string): TerminalSession[];
|
|
20
|
+
attachWebSocket(sessionId: string, ws: WebSocket): boolean;
|
|
21
|
+
destroySession(sessionId: string): boolean;
|
|
22
|
+
resizeSession(sessionId: string, cols: number, rows: number): boolean;
|
|
23
|
+
private cleanupInactiveSessions;
|
|
24
|
+
getAllSessions(): TerminalSession[];
|
|
25
|
+
destroy(): void;
|
|
26
|
+
}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import * as pty from 'node-pty';
|
|
2
|
+
import { WebSocket } from 'ws';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { createInitScript } from './context-helper.js';
|
|
6
|
+
import { getShellPath, getShellArgs, prepareShellInit } from './shell-utils.js';
|
|
7
|
+
export class TerminalManager {
|
|
8
|
+
sessions = new Map();
|
|
9
|
+
cleanupInterval;
|
|
10
|
+
constructor() {
|
|
11
|
+
// Clean up inactive sessions every 5 minutes
|
|
12
|
+
this.cleanupInterval = setInterval(() => {
|
|
13
|
+
this.cleanupInactiveSessions();
|
|
14
|
+
}, 5 * 60 * 1000);
|
|
15
|
+
}
|
|
16
|
+
async createSession(proposalId, enableLogging = false, shellPreference = 'bash') {
|
|
17
|
+
const sessionId = `${proposalId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
18
|
+
// Get the project root directory
|
|
19
|
+
const projectRoot = process.env.GAIT_DATA_PATH ?
|
|
20
|
+
path.resolve(process.env.GAIT_DATA_PATH) :
|
|
21
|
+
process.cwd();
|
|
22
|
+
// Prepare context information
|
|
23
|
+
const contextInfo = {
|
|
24
|
+
proposalId,
|
|
25
|
+
proposalPath: path.join(projectRoot, '.gait', 'proposals', `${proposalId}.md`),
|
|
26
|
+
contextPath: path.join(projectRoot, '.gait', 'context'),
|
|
27
|
+
projectRoot,
|
|
28
|
+
};
|
|
29
|
+
// Get the shell path based on user preference
|
|
30
|
+
const shellPath = await getShellPath(shellPreference);
|
|
31
|
+
const shellName = shellPath.split('/').pop() || 'bash';
|
|
32
|
+
// Create initialization script
|
|
33
|
+
const baseInitScript = createInitScript(contextInfo);
|
|
34
|
+
const { script: initScript, needsEnvVar } = prepareShellInit(shellPath, baseInitScript);
|
|
35
|
+
// Determine init script path and name based on shell
|
|
36
|
+
let initScriptPath;
|
|
37
|
+
let tempDirForZsh;
|
|
38
|
+
if (shellName === 'zsh' && needsEnvVar) {
|
|
39
|
+
// For zsh, create a temp directory with .zshrc
|
|
40
|
+
tempDirForZsh = path.join(projectRoot, '.gait', 'tmp', `zsh-${sessionId}`);
|
|
41
|
+
fs.mkdirSync(tempDirForZsh, { recursive: true });
|
|
42
|
+
initScriptPath = path.join(tempDirForZsh, '.zshrc');
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
// For other shells, use regular init script
|
|
46
|
+
initScriptPath = path.join(projectRoot, '.gait', 'tmp', `init-${sessionId}.sh`);
|
|
47
|
+
const tmpDir = path.dirname(initScriptPath);
|
|
48
|
+
fs.mkdirSync(tmpDir, { recursive: true });
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
fs.writeFileSync(initScriptPath, initScript);
|
|
52
|
+
fs.chmodSync(initScriptPath, '755');
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
console.error('Error creating init script:', error);
|
|
56
|
+
}
|
|
57
|
+
// Set up logging if enabled
|
|
58
|
+
let logFilePath;
|
|
59
|
+
if (enableLogging) {
|
|
60
|
+
const sessionsDir = path.join(projectRoot, '.gait', 'sessions');
|
|
61
|
+
try {
|
|
62
|
+
fs.mkdirSync(sessionsDir, { recursive: true });
|
|
63
|
+
logFilePath = path.join(sessionsDir, `${sessionId}.log`);
|
|
64
|
+
// Create log file with session header
|
|
65
|
+
const logHeader = `# Terminal Session Log\n# Session ID: ${sessionId}\n# Proposal ID: ${proposalId}\n# Created: ${new Date().toISOString()}\n\n`;
|
|
66
|
+
fs.writeFileSync(logFilePath, logHeader);
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error('Error setting up session logging:', error);
|
|
70
|
+
logFilePath = undefined;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
// Get shell arguments
|
|
74
|
+
const shellArgs = getShellArgs(shellPath, initScriptPath);
|
|
75
|
+
// Prepare environment variables
|
|
76
|
+
const env = {
|
|
77
|
+
...process.env,
|
|
78
|
+
GAIT_PROPOSAL_ID: proposalId,
|
|
79
|
+
GAIT_CONTEXT_PATH: path.join(projectRoot, '.gait', 'context'),
|
|
80
|
+
GAIT_PROPOSAL_PATH: path.join(projectRoot, '.gait', 'proposals', `${proposalId}.md`),
|
|
81
|
+
TERM: 'xterm-256color',
|
|
82
|
+
COLORTERM: 'truecolor',
|
|
83
|
+
};
|
|
84
|
+
// Add special environment variable for zsh if needed
|
|
85
|
+
if (tempDirForZsh) {
|
|
86
|
+
env.ZDOTDIR = tempDirForZsh;
|
|
87
|
+
}
|
|
88
|
+
// Create PTY with environment variables for context
|
|
89
|
+
const ptyProcess = pty.spawn(shellPath, shellArgs, {
|
|
90
|
+
name: 'xterm-color',
|
|
91
|
+
cols: 80,
|
|
92
|
+
rows: 24,
|
|
93
|
+
cwd: projectRoot,
|
|
94
|
+
env,
|
|
95
|
+
});
|
|
96
|
+
const session = {
|
|
97
|
+
id: sessionId,
|
|
98
|
+
proposalId,
|
|
99
|
+
pty: ptyProcess,
|
|
100
|
+
createdAt: new Date(),
|
|
101
|
+
lastActivity: new Date(),
|
|
102
|
+
enableLogging,
|
|
103
|
+
logFilePath,
|
|
104
|
+
};
|
|
105
|
+
this.sessions.set(sessionId, session);
|
|
106
|
+
// Set up PTY event handlers
|
|
107
|
+
ptyProcess.onData((data) => {
|
|
108
|
+
session.lastActivity = new Date();
|
|
109
|
+
// Log data if logging is enabled
|
|
110
|
+
if (session.enableLogging && session.logFilePath) {
|
|
111
|
+
try {
|
|
112
|
+
const timestamp = new Date().toISOString();
|
|
113
|
+
const logEntry = `[${timestamp}] OUTPUT: ${data}`;
|
|
114
|
+
fs.appendFileSync(session.logFilePath, logEntry);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
console.error('Error writing to session log:', error);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (session.websocket && session.websocket.readyState === WebSocket.OPEN) {
|
|
121
|
+
session.websocket.send(JSON.stringify({
|
|
122
|
+
type: 'data',
|
|
123
|
+
data: data,
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
ptyProcess.onExit((e) => {
|
|
128
|
+
// Log session end if logging is enabled
|
|
129
|
+
if (session.enableLogging && session.logFilePath) {
|
|
130
|
+
try {
|
|
131
|
+
const timestamp = new Date().toISOString();
|
|
132
|
+
const logEntry = `\n[${timestamp}] SESSION ENDED: Exit code ${e.exitCode}\n`;
|
|
133
|
+
fs.appendFileSync(session.logFilePath, logEntry);
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
console.error('Error writing session end to log:', error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Clean up init script and temp directories
|
|
140
|
+
try {
|
|
141
|
+
if (fs.existsSync(initScriptPath)) {
|
|
142
|
+
fs.unlinkSync(initScriptPath);
|
|
143
|
+
}
|
|
144
|
+
// Clean up zsh temp directory if it exists
|
|
145
|
+
if (tempDirForZsh && fs.existsSync(tempDirForZsh)) {
|
|
146
|
+
fs.rmSync(tempDirForZsh, { recursive: true, force: true });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
console.error('Error cleaning up init script:', error);
|
|
151
|
+
}
|
|
152
|
+
if (session.websocket && session.websocket.readyState === WebSocket.OPEN) {
|
|
153
|
+
session.websocket.send(JSON.stringify({
|
|
154
|
+
type: 'exit',
|
|
155
|
+
exitCode: e.exitCode,
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
this.destroySession(sessionId);
|
|
159
|
+
});
|
|
160
|
+
return session;
|
|
161
|
+
}
|
|
162
|
+
getSession(sessionId) {
|
|
163
|
+
return this.sessions.get(sessionId);
|
|
164
|
+
}
|
|
165
|
+
getSessionsByProposal(proposalId) {
|
|
166
|
+
return Array.from(this.sessions.values()).filter(session => session.proposalId === proposalId);
|
|
167
|
+
}
|
|
168
|
+
attachWebSocket(sessionId, ws) {
|
|
169
|
+
const session = this.sessions.get(sessionId);
|
|
170
|
+
if (!session) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
session.websocket = ws;
|
|
174
|
+
session.lastActivity = new Date();
|
|
175
|
+
// Set up WebSocket event handlers
|
|
176
|
+
ws.on('message', (message) => {
|
|
177
|
+
try {
|
|
178
|
+
const data = JSON.parse(message.toString());
|
|
179
|
+
switch (data.type) {
|
|
180
|
+
case 'input':
|
|
181
|
+
// Log input if logging is enabled
|
|
182
|
+
if (session.enableLogging && session.logFilePath) {
|
|
183
|
+
try {
|
|
184
|
+
const timestamp = new Date().toISOString();
|
|
185
|
+
const logEntry = `[${timestamp}] INPUT: ${data.data}`;
|
|
186
|
+
fs.appendFileSync(session.logFilePath, logEntry);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
console.error('Error writing input to session log:', error);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
session.pty.write(data.data);
|
|
193
|
+
session.lastActivity = new Date();
|
|
194
|
+
break;
|
|
195
|
+
case 'resize':
|
|
196
|
+
session.pty.resize(data.cols, data.rows);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
console.error('Error processing WebSocket message:', error);
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
ws.on('close', () => {
|
|
205
|
+
if (session.websocket === ws) {
|
|
206
|
+
session.websocket = undefined;
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
ws.on('error', (error) => {
|
|
210
|
+
console.error('WebSocket error:', error);
|
|
211
|
+
if (session.websocket === ws) {
|
|
212
|
+
session.websocket = undefined;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
destroySession(sessionId) {
|
|
218
|
+
const session = this.sessions.get(sessionId);
|
|
219
|
+
if (!session) {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
// Close WebSocket if connected
|
|
223
|
+
if (session.websocket && session.websocket.readyState === WebSocket.OPEN) {
|
|
224
|
+
session.websocket.close();
|
|
225
|
+
}
|
|
226
|
+
// Kill PTY process
|
|
227
|
+
try {
|
|
228
|
+
session.pty.kill();
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
console.error('Error killing PTY process:', error);
|
|
232
|
+
}
|
|
233
|
+
this.sessions.delete(sessionId);
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
resizeSession(sessionId, cols, rows) {
|
|
237
|
+
const session = this.sessions.get(sessionId);
|
|
238
|
+
if (!session) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
try {
|
|
242
|
+
session.pty.resize(cols, rows);
|
|
243
|
+
session.lastActivity = new Date();
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
console.error('Error resizing terminal:', error);
|
|
248
|
+
return false;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
cleanupInactiveSessions() {
|
|
252
|
+
const now = new Date();
|
|
253
|
+
const maxInactiveTime = 30 * 60 * 1000; // 30 minutes
|
|
254
|
+
for (const [sessionId, session] of Array.from(this.sessions.entries())) {
|
|
255
|
+
const inactiveTime = now.getTime() - session.lastActivity.getTime();
|
|
256
|
+
if (inactiveTime > maxInactiveTime) {
|
|
257
|
+
console.log(`Cleaning up inactive session: ${sessionId}`);
|
|
258
|
+
this.destroySession(sessionId);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
getAllSessions() {
|
|
263
|
+
return Array.from(this.sessions.values());
|
|
264
|
+
}
|
|
265
|
+
destroy() {
|
|
266
|
+
// Clean up all sessions
|
|
267
|
+
for (const sessionId of Array.from(this.sessions.keys())) {
|
|
268
|
+
this.destroySession(sessionId);
|
|
269
|
+
}
|
|
270
|
+
// Clear cleanup interval
|
|
271
|
+
if (this.cleanupInterval) {
|
|
272
|
+
clearInterval(this.cleanupInterval);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// We'll use the global manager instead of creating a direct export
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export interface UserProfile {
|
|
2
|
+
firstName?: string;
|
|
3
|
+
lastName?: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
role?: 'developer' | 'designer' | 'product-manager' | 'other';
|
|
6
|
+
}
|
|
7
|
+
export interface UserPreferences {
|
|
8
|
+
theme?: 'light' | 'dark' | 'coconut' | 'system';
|
|
9
|
+
defaultEditor?: 'cursor' | 'vscode' | 'vim' | 'emacs';
|
|
10
|
+
terminalShell?: 'bash' | 'zsh' | 'fish' | 'sh' | 'system';
|
|
11
|
+
autoSaveInterval?: number;
|
|
12
|
+
notifications?: {
|
|
13
|
+
email?: boolean;
|
|
14
|
+
desktop?: boolean;
|
|
15
|
+
slack?: boolean;
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
export interface UserSettings {
|
|
19
|
+
profile: UserProfile;
|
|
20
|
+
preferences: UserPreferences;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Ensure the ~/.gait directory exists
|
|
24
|
+
*/
|
|
25
|
+
export declare function ensureGaitHomeDirectory(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Load user settings from ~/.gait/user-settings.json
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadUserSettings(): Promise<UserSettings>;
|
|
30
|
+
/**
|
|
31
|
+
* Save user settings to ~/.gait/user-settings.json
|
|
32
|
+
*/
|
|
33
|
+
export declare function saveUserSettings(settings: UserSettings): Promise<void>;
|
|
34
|
+
/**
|
|
35
|
+
* Update user profile information
|
|
36
|
+
*/
|
|
37
|
+
export declare function updateUserProfile(profile: Partial<UserProfile>): Promise<UserSettings>;
|
|
38
|
+
/**
|
|
39
|
+
* Update user preferences
|
|
40
|
+
*/
|
|
41
|
+
export declare function updateUserPreferences(preferences: Partial<UserPreferences>): Promise<UserSettings>;
|
|
42
|
+
/**
|
|
43
|
+
* Get the author information for Change Proposals
|
|
44
|
+
*/
|
|
45
|
+
export declare function getAuthorInfo(): Promise<{
|
|
46
|
+
name: string;
|
|
47
|
+
email: string;
|
|
48
|
+
}>;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
const GAIT_HOME_DIR = path.join(os.homedir(), '.gait');
|
|
5
|
+
const USER_SETTINGS_FILE = path.join(GAIT_HOME_DIR, 'user-settings.json');
|
|
6
|
+
/**
|
|
7
|
+
* Ensure the ~/.gait directory exists
|
|
8
|
+
*/
|
|
9
|
+
export async function ensureGaitHomeDirectory() {
|
|
10
|
+
try {
|
|
11
|
+
await fs.access(GAIT_HOME_DIR);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
await fs.mkdir(GAIT_HOME_DIR, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Load user settings from ~/.gait/user-settings.json
|
|
19
|
+
*/
|
|
20
|
+
export async function loadUserSettings() {
|
|
21
|
+
try {
|
|
22
|
+
await ensureGaitHomeDirectory();
|
|
23
|
+
const data = await fs.readFile(USER_SETTINGS_FILE, 'utf-8');
|
|
24
|
+
return JSON.parse(data);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// Return default settings if file doesn't exist or is invalid
|
|
28
|
+
return {
|
|
29
|
+
profile: {},
|
|
30
|
+
preferences: {
|
|
31
|
+
theme: 'system',
|
|
32
|
+
defaultEditor: 'cursor',
|
|
33
|
+
terminalShell: 'bash',
|
|
34
|
+
autoSaveInterval: 30,
|
|
35
|
+
notifications: {
|
|
36
|
+
email: true,
|
|
37
|
+
desktop: true,
|
|
38
|
+
slack: false,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Save user settings to ~/.gait/user-settings.json
|
|
46
|
+
*/
|
|
47
|
+
export async function saveUserSettings(settings) {
|
|
48
|
+
await ensureGaitHomeDirectory();
|
|
49
|
+
await fs.writeFile(USER_SETTINGS_FILE, JSON.stringify(settings, null, 2), 'utf-8');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Update user profile information
|
|
53
|
+
*/
|
|
54
|
+
export async function updateUserProfile(profile) {
|
|
55
|
+
const settings = await loadUserSettings();
|
|
56
|
+
settings.profile = { ...settings.profile, ...profile };
|
|
57
|
+
await saveUserSettings(settings);
|
|
58
|
+
return settings;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Update user preferences
|
|
62
|
+
*/
|
|
63
|
+
export async function updateUserPreferences(preferences) {
|
|
64
|
+
const settings = await loadUserSettings();
|
|
65
|
+
settings.preferences = {
|
|
66
|
+
...settings.preferences,
|
|
67
|
+
...preferences,
|
|
68
|
+
notifications: {
|
|
69
|
+
...settings.preferences.notifications,
|
|
70
|
+
...preferences.notifications,
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
await saveUserSettings(settings);
|
|
74
|
+
return settings;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Get the author information for Change Proposals
|
|
78
|
+
*/
|
|
79
|
+
export async function getAuthorInfo() {
|
|
80
|
+
const settings = await loadUserSettings();
|
|
81
|
+
const { firstName, lastName, email } = settings.profile;
|
|
82
|
+
const name = [firstName, lastName].filter(Boolean).join(' ') || 'Current User';
|
|
83
|
+
return {
|
|
84
|
+
name,
|
|
85
|
+
email: email || 'user@example.com',
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function GET(): Promise<any>;
|