@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,444 @@
|
|
|
1
|
+
import { promises as fs } from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
/**
|
|
5
|
+
* SymlinkManager handles all symlink operations and persists state
|
|
6
|
+
*/
|
|
7
|
+
export class SymlinkManager {
|
|
8
|
+
configPath;
|
|
9
|
+
projectRoot;
|
|
10
|
+
state;
|
|
11
|
+
constructor(projectRoot) {
|
|
12
|
+
this.projectRoot = projectRoot;
|
|
13
|
+
this.configPath = path.join(projectRoot, '.gait', 'symlinks.json');
|
|
14
|
+
this.state = {
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
symlinks: []
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Initialize the manager and load existing configuration
|
|
21
|
+
*/
|
|
22
|
+
async initialize() {
|
|
23
|
+
try {
|
|
24
|
+
console.log(`[SymlinkManager] Initializing with project root: ${this.projectRoot}`);
|
|
25
|
+
console.log(`[SymlinkManager] Config path: ${this.configPath}`);
|
|
26
|
+
// Ensure .gait directory exists
|
|
27
|
+
const gaitDir = path.join(this.projectRoot, '.gait');
|
|
28
|
+
await fs.mkdir(gaitDir, { recursive: true });
|
|
29
|
+
// Try to load existing configuration
|
|
30
|
+
try {
|
|
31
|
+
const configData = await fs.readFile(this.configPath, 'utf-8');
|
|
32
|
+
this.state = JSON.parse(configData);
|
|
33
|
+
console.log(`[SymlinkManager] Loaded ${this.state.symlinks.length} symlinks from config`);
|
|
34
|
+
// Validate state against actual filesystem
|
|
35
|
+
await this.validateSymlinks();
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error.code === 'ENOENT') {
|
|
39
|
+
console.log('[SymlinkManager] No existing config found, creating default');
|
|
40
|
+
// Config doesn't exist, create default
|
|
41
|
+
await this.createDefaultConfig();
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
console.error('[SymlinkManager] Error loading config:', error);
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
console.error('[SymlinkManager] Failed to initialize:', error);
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Create default configuration with the CLAUDE.md symlink
|
|
56
|
+
*/
|
|
57
|
+
async createDefaultConfig() {
|
|
58
|
+
// Check if the legacy CLAUDE.md symlink exists
|
|
59
|
+
const claudeSymlinkPath = path.join(this.projectRoot, 'CLAUDE.md');
|
|
60
|
+
let isActive = false;
|
|
61
|
+
try {
|
|
62
|
+
const stats = await fs.lstat(claudeSymlinkPath);
|
|
63
|
+
isActive = stats.isSymbolicLink();
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// Symlink doesn't exist
|
|
67
|
+
}
|
|
68
|
+
const defaultSymlink = {
|
|
69
|
+
id: 'claude-rules',
|
|
70
|
+
name: 'Claude Rules',
|
|
71
|
+
description: 'Project-specific rules and guidelines for Claude AI assistant',
|
|
72
|
+
linkPath: 'CLAUDE.md',
|
|
73
|
+
targetPath: '.gait/rules/CLAUDE.md',
|
|
74
|
+
isActive,
|
|
75
|
+
createdAt: new Date().toISOString(),
|
|
76
|
+
updatedAt: new Date().toISOString()
|
|
77
|
+
};
|
|
78
|
+
this.state.symlinks = [defaultSymlink];
|
|
79
|
+
await this.saveState();
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Validate symlinks against actual filesystem state
|
|
83
|
+
*/
|
|
84
|
+
async validateSymlinks() {
|
|
85
|
+
for (const symlink of this.state.symlinks) {
|
|
86
|
+
// Expand tilde and resolve path
|
|
87
|
+
const expandedLinkPath = this.expandTilde(symlink.linkPath);
|
|
88
|
+
const fullLinkPath = path.isAbsolute(expandedLinkPath)
|
|
89
|
+
? expandedLinkPath
|
|
90
|
+
: path.join(this.projectRoot, expandedLinkPath);
|
|
91
|
+
try {
|
|
92
|
+
const stats = await fs.lstat(fullLinkPath);
|
|
93
|
+
const actuallyActive = stats.isSymbolicLink();
|
|
94
|
+
if (actuallyActive !== symlink.isActive) {
|
|
95
|
+
// State mismatch - filesystem is source of truth
|
|
96
|
+
symlink.isActive = actuallyActive;
|
|
97
|
+
symlink.updatedAt = new Date().toISOString();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (error.code === 'ENOENT') {
|
|
102
|
+
// File doesn't exist
|
|
103
|
+
if (symlink.isActive) {
|
|
104
|
+
symlink.isActive = false;
|
|
105
|
+
symlink.updatedAt = new Date().toISOString();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
await this.saveState();
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Save current state to configuration file
|
|
114
|
+
*/
|
|
115
|
+
async saveState() {
|
|
116
|
+
try {
|
|
117
|
+
// Ensure directory exists
|
|
118
|
+
await fs.mkdir(path.dirname(this.configPath), { recursive: true });
|
|
119
|
+
// Write to temp file first
|
|
120
|
+
const tempPath = `${this.configPath}.tmp`;
|
|
121
|
+
await fs.writeFile(tempPath, JSON.stringify(this.state, null, 2), 'utf-8');
|
|
122
|
+
// Atomic rename
|
|
123
|
+
await fs.rename(tempPath, this.configPath);
|
|
124
|
+
console.log(`[SymlinkManager] Saved state with ${this.state.symlinks.length} symlinks to ${this.configPath}`);
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
console.error('[SymlinkManager] Failed to save state:', error);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Get all symlink configurations
|
|
133
|
+
*/
|
|
134
|
+
async getSymlinks() {
|
|
135
|
+
await this.validateSymlinks();
|
|
136
|
+
return this.state.symlinks;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Get a specific symlink by ID
|
|
140
|
+
*/
|
|
141
|
+
async getSymlink(id) {
|
|
142
|
+
await this.validateSymlinks();
|
|
143
|
+
return this.state.symlinks.find(s => s.id === id);
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Add a new symlink configuration
|
|
147
|
+
*/
|
|
148
|
+
async addSymlink(config) {
|
|
149
|
+
try {
|
|
150
|
+
// Check if link path already exists
|
|
151
|
+
if (this.state.symlinks.some(s => s.linkPath === config.linkPath)) {
|
|
152
|
+
return {
|
|
153
|
+
success: false,
|
|
154
|
+
message: `A symlink at ${config.linkPath} already exists`
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
const newSymlink = {
|
|
158
|
+
...config,
|
|
159
|
+
id: `symlink-${Date.now()}`,
|
|
160
|
+
createdAt: new Date().toISOString(),
|
|
161
|
+
updatedAt: new Date().toISOString()
|
|
162
|
+
};
|
|
163
|
+
this.state.symlinks.push(newSymlink);
|
|
164
|
+
if (config.isActive) {
|
|
165
|
+
const result = await this.createSymlink(newSymlink.id);
|
|
166
|
+
if (!result.success) {
|
|
167
|
+
// Roll back
|
|
168
|
+
this.state.symlinks = this.state.symlinks.filter(s => s.id !== newSymlink.id);
|
|
169
|
+
return result;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
await this.saveState();
|
|
173
|
+
return {
|
|
174
|
+
success: true,
|
|
175
|
+
message: 'Symlink configuration added successfully',
|
|
176
|
+
symlink: newSymlink
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
message: 'Failed to add symlink',
|
|
183
|
+
error: error.message
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Toggle a symlink on/off
|
|
189
|
+
*/
|
|
190
|
+
async toggleSymlink(id) {
|
|
191
|
+
const symlink = this.state.symlinks.find(s => s.id === id);
|
|
192
|
+
if (!symlink) {
|
|
193
|
+
return {
|
|
194
|
+
success: false,
|
|
195
|
+
message: `Symlink with ID ${id} not found`
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (symlink.isActive) {
|
|
199
|
+
return await this.removeSymlink(id);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
return await this.createSymlink(id);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Expand tilde in path to actual home directory
|
|
207
|
+
*/
|
|
208
|
+
expandTilde(filepath) {
|
|
209
|
+
if (filepath.startsWith('~/')) {
|
|
210
|
+
return path.join(os.homedir(), filepath.slice(2));
|
|
211
|
+
}
|
|
212
|
+
return filepath;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Create a symlink on the filesystem
|
|
216
|
+
*/
|
|
217
|
+
async createSymlink(id) {
|
|
218
|
+
const symlink = this.state.symlinks.find(s => s.id === id);
|
|
219
|
+
if (!symlink) {
|
|
220
|
+
return {
|
|
221
|
+
success: false,
|
|
222
|
+
message: `Symlink with ID ${id} not found`
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
try {
|
|
226
|
+
// Expand tilde in paths and resolve them
|
|
227
|
+
const expandedLinkPath = this.expandTilde(symlink.linkPath);
|
|
228
|
+
const expandedTargetPath = this.expandTilde(symlink.targetPath);
|
|
229
|
+
// If paths are relative (not starting with / or ~), make them relative to project root
|
|
230
|
+
const fullLinkPath = path.isAbsolute(expandedLinkPath)
|
|
231
|
+
? expandedLinkPath
|
|
232
|
+
: path.join(this.projectRoot, expandedLinkPath);
|
|
233
|
+
const fullTargetPath = path.isAbsolute(expandedTargetPath)
|
|
234
|
+
? expandedTargetPath
|
|
235
|
+
: path.join(this.projectRoot, expandedTargetPath);
|
|
236
|
+
// Ensure target exists
|
|
237
|
+
try {
|
|
238
|
+
await fs.access(fullTargetPath);
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
// Create target directory and file if needed
|
|
242
|
+
await fs.mkdir(path.dirname(fullTargetPath), { recursive: true });
|
|
243
|
+
// Create a default file if it doesn't exist
|
|
244
|
+
await fs.writeFile(fullTargetPath, `# ${symlink.name}
|
|
245
|
+
|
|
246
|
+
${symlink.description || 'Configuration file'}
|
|
247
|
+
|
|
248
|
+
Created: ${new Date().toISOString()}
|
|
249
|
+
`);
|
|
250
|
+
}
|
|
251
|
+
// Remove existing file/symlink if it exists
|
|
252
|
+
try {
|
|
253
|
+
await fs.unlink(fullLinkPath);
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
if (error.code !== 'ENOENT')
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
// Create the symlink (use relative path for portability)
|
|
260
|
+
const relativePath = path.relative(path.dirname(fullLinkPath), fullTargetPath);
|
|
261
|
+
await fs.symlink(relativePath, fullLinkPath);
|
|
262
|
+
symlink.isActive = true;
|
|
263
|
+
symlink.updatedAt = new Date().toISOString();
|
|
264
|
+
await this.saveState();
|
|
265
|
+
return {
|
|
266
|
+
success: true,
|
|
267
|
+
message: `Symlink ${symlink.name} created successfully`,
|
|
268
|
+
symlink
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
return {
|
|
273
|
+
success: false,
|
|
274
|
+
message: `Failed to create symlink`,
|
|
275
|
+
error: error.message
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Remove a symlink from the filesystem
|
|
281
|
+
*/
|
|
282
|
+
async removeSymlink(id) {
|
|
283
|
+
const symlink = this.state.symlinks.find(s => s.id === id);
|
|
284
|
+
if (!symlink) {
|
|
285
|
+
return {
|
|
286
|
+
success: false,
|
|
287
|
+
message: `Symlink with ID ${id} not found`
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
try {
|
|
291
|
+
// Expand tilde and resolve path
|
|
292
|
+
const expandedLinkPath = this.expandTilde(symlink.linkPath);
|
|
293
|
+
const fullLinkPath = path.isAbsolute(expandedLinkPath)
|
|
294
|
+
? expandedLinkPath
|
|
295
|
+
: path.join(this.projectRoot, expandedLinkPath);
|
|
296
|
+
try {
|
|
297
|
+
const stats = await fs.lstat(fullLinkPath);
|
|
298
|
+
if (stats.isSymbolicLink()) {
|
|
299
|
+
await fs.unlink(fullLinkPath);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (error.code !== 'ENOENT')
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
symlink.isActive = false;
|
|
307
|
+
symlink.updatedAt = new Date().toISOString();
|
|
308
|
+
await this.saveState();
|
|
309
|
+
return {
|
|
310
|
+
success: true,
|
|
311
|
+
message: `Symlink ${symlink.name} removed successfully`,
|
|
312
|
+
symlink
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
catch (error) {
|
|
316
|
+
return {
|
|
317
|
+
success: false,
|
|
318
|
+
message: `Failed to remove symlink`,
|
|
319
|
+
error: error.message
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Delete a symlink configuration entirely
|
|
325
|
+
*/
|
|
326
|
+
async deleteSymlink(id) {
|
|
327
|
+
const symlink = this.state.symlinks.find(s => s.id === id);
|
|
328
|
+
if (!symlink) {
|
|
329
|
+
return {
|
|
330
|
+
success: false,
|
|
331
|
+
message: `Symlink with ID ${id} not found`
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
// Remove from filesystem first if active
|
|
335
|
+
if (symlink.isActive) {
|
|
336
|
+
await this.removeSymlink(id);
|
|
337
|
+
}
|
|
338
|
+
// Remove from configuration
|
|
339
|
+
this.state.symlinks = this.state.symlinks.filter(s => s.id !== id);
|
|
340
|
+
await this.saveState();
|
|
341
|
+
return {
|
|
342
|
+
success: true,
|
|
343
|
+
message: `Symlink configuration deleted successfully`
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Update a symlink configuration
|
|
348
|
+
*/
|
|
349
|
+
async updateSymlink(id, updates) {
|
|
350
|
+
const symlink = this.state.symlinks.find(s => s.id === id);
|
|
351
|
+
if (!symlink) {
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
message: `Symlink with ID ${id} not found`
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
try {
|
|
358
|
+
// If changing paths and symlink is active, need to recreate
|
|
359
|
+
if (symlink.isActive && (updates.linkPath || updates.targetPath)) {
|
|
360
|
+
await this.removeSymlink(id);
|
|
361
|
+
}
|
|
362
|
+
// Apply updates
|
|
363
|
+
Object.assign(symlink, updates, {
|
|
364
|
+
updatedAt: new Date().toISOString()
|
|
365
|
+
});
|
|
366
|
+
// Recreate if was active and paths changed
|
|
367
|
+
if (symlink.isActive && (updates.linkPath || updates.targetPath)) {
|
|
368
|
+
const result = await this.createSymlink(id);
|
|
369
|
+
if (!result.success) {
|
|
370
|
+
return result;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
await this.saveState();
|
|
374
|
+
return {
|
|
375
|
+
success: true,
|
|
376
|
+
message: 'Symlink updated successfully',
|
|
377
|
+
symlink
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
catch (error) {
|
|
381
|
+
return {
|
|
382
|
+
success: false,
|
|
383
|
+
message: 'Failed to update symlink',
|
|
384
|
+
error: error.message
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
// Singleton instance and initialization promise
|
|
390
|
+
let managerInstance = null;
|
|
391
|
+
let initializationPromise = null;
|
|
392
|
+
/**
|
|
393
|
+
* Get or create the SymlinkManager instance
|
|
394
|
+
*/
|
|
395
|
+
export async function getSymlinkManager() {
|
|
396
|
+
// If already initializing, wait for that to complete
|
|
397
|
+
if (initializationPromise) {
|
|
398
|
+
return initializationPromise;
|
|
399
|
+
}
|
|
400
|
+
// If already initialized, return the instance
|
|
401
|
+
if (managerInstance) {
|
|
402
|
+
return managerInstance;
|
|
403
|
+
}
|
|
404
|
+
// Start initialization
|
|
405
|
+
initializationPromise = (async () => {
|
|
406
|
+
try {
|
|
407
|
+
// Determine project root more reliably
|
|
408
|
+
let projectRoot = process.env.GAIT_PROJECT_ROOT;
|
|
409
|
+
if (!projectRoot) {
|
|
410
|
+
// Try to find the project root by looking for distinctive files
|
|
411
|
+
const cwd = process.cwd();
|
|
412
|
+
// Check if we're in packages/web
|
|
413
|
+
if (cwd.includes('packages/web')) {
|
|
414
|
+
projectRoot = path.resolve(cwd, '../..');
|
|
415
|
+
}
|
|
416
|
+
else if (cwd.includes('packages')) {
|
|
417
|
+
projectRoot = path.resolve(cwd, '..');
|
|
418
|
+
}
|
|
419
|
+
else {
|
|
420
|
+
projectRoot = cwd;
|
|
421
|
+
}
|
|
422
|
+
// Verify this is the right directory by checking for .gait
|
|
423
|
+
try {
|
|
424
|
+
await fs.access(path.join(projectRoot, '.gait'));
|
|
425
|
+
}
|
|
426
|
+
catch {
|
|
427
|
+
console.warn(`Could not find .gait directory at ${projectRoot}, using cwd`);
|
|
428
|
+
projectRoot = cwd;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
console.log(`[SymlinkManager] Using project root: ${projectRoot}`);
|
|
432
|
+
managerInstance = new SymlinkManager(projectRoot);
|
|
433
|
+
await managerInstance.initialize();
|
|
434
|
+
return managerInstance;
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
// Reset on error so we can retry
|
|
438
|
+
initializationPromise = null;
|
|
439
|
+
managerInstance = null;
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
})();
|
|
443
|
+
return initializationPromise;
|
|
444
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Symlink configuration types
|
|
3
|
+
*/
|
|
4
|
+
export interface SymlinkConfig {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
linkPath: string;
|
|
9
|
+
targetPath: string;
|
|
10
|
+
isActive: boolean;
|
|
11
|
+
createdAt: string;
|
|
12
|
+
updatedAt: string;
|
|
13
|
+
}
|
|
14
|
+
export interface SymlinksState {
|
|
15
|
+
version: string;
|
|
16
|
+
symlinks: SymlinkConfig[];
|
|
17
|
+
}
|
|
18
|
+
export interface SymlinkOperationResult {
|
|
19
|
+
success: boolean;
|
|
20
|
+
message: string;
|
|
21
|
+
error?: string;
|
|
22
|
+
symlink?: SymlinkConfig;
|
|
23
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export interface ContextInfo {
|
|
2
|
+
proposalId: string;
|
|
3
|
+
proposalPath: string;
|
|
4
|
+
contextPath: string;
|
|
5
|
+
projectRoot: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function generateWelcomeMessage(context: ContextInfo): string;
|
|
8
|
+
export declare function generateClaudeSetupCommands(context: ContextInfo): string[];
|
|
9
|
+
export declare function generateContextCommands(_context: ContextInfo): string[];
|
|
10
|
+
export declare function generateBashAliases(_context: ContextInfo): string[];
|
|
11
|
+
export declare function createInitScript(context: ContextInfo): string;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
// Context helper functions for terminal sessions
|
|
2
|
+
// These can be used to generate helpful commands and information
|
|
3
|
+
export function generateWelcomeMessage(context) {
|
|
4
|
+
return `
|
|
5
|
+
\x1b[32m╭─────────────────────────────────────────────────────────────╮\x1b[0m
|
|
6
|
+
\x1b[32m│ GAIT Terminal Session │\x1b[0m
|
|
7
|
+
\x1b[32m╰─────────────────────────────────────────────────────────────╯\x1b[0m
|
|
8
|
+
|
|
9
|
+
\x1b[36mProposal:\x1b[0m ${context.proposalId}
|
|
10
|
+
\x1b[36mProject Root:\x1b[0m ${context.projectRoot}
|
|
11
|
+
|
|
12
|
+
\x1b[33mAvailable Environment Variables:\x1b[0m
|
|
13
|
+
GAIT_PROPOSAL_ID="${context.proposalId}"
|
|
14
|
+
GAIT_CONTEXT_PATH="${context.contextPath}"
|
|
15
|
+
GAIT_PROPOSAL_PATH="${context.proposalPath}"
|
|
16
|
+
|
|
17
|
+
\x1b[33mQuick Commands:\x1b[0m
|
|
18
|
+
\x1b[32mgait-help\x1b[0m - Show this help message
|
|
19
|
+
\x1b[32mgait-proposal\x1b[0m - View current proposal
|
|
20
|
+
\x1b[32mgait-files\x1b[0m - List context files
|
|
21
|
+
\x1b[32mgait-claude\x1b[0m - Setup Claude Code CLI
|
|
22
|
+
|
|
23
|
+
\x1b[90mTip: Use 'gait-claude' to configure Claude Code for this proposal\x1b[0m
|
|
24
|
+
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
export function generateClaudeSetupCommands(context) {
|
|
28
|
+
return [
|
|
29
|
+
'# Claude Code CLI Setup Commands',
|
|
30
|
+
'# Run these commands to configure Claude Code for this proposal:',
|
|
31
|
+
'',
|
|
32
|
+
'# 1. Set the working directory context',
|
|
33
|
+
`export CLAUDE_CONTEXT_PATH="${context.contextPath}"`,
|
|
34
|
+
`export CLAUDE_PROPOSAL_PATH="${context.proposalPath}"`,
|
|
35
|
+
'',
|
|
36
|
+
'# 2. Create a Claude Code project configuration',
|
|
37
|
+
'cat > .claude-project.json << EOF',
|
|
38
|
+
'{',
|
|
39
|
+
` "name": "GAIT Proposal ${context.proposalId}",`,
|
|
40
|
+
` "description": "Working on proposal ${context.proposalId}",`,
|
|
41
|
+
' "context": [',
|
|
42
|
+
` "${context.proposalPath}",`,
|
|
43
|
+
` "${context.contextPath}/project.md",`,
|
|
44
|
+
` "${context.contextPath}/architecture.md",`,
|
|
45
|
+
' ],',
|
|
46
|
+
' "rules": [',
|
|
47
|
+
' "Follow the project architecture patterns",',
|
|
48
|
+
' "Reference the proposal requirements",',
|
|
49
|
+
' "Maintain consistency with existing code"',
|
|
50
|
+
' ]',
|
|
51
|
+
'}',
|
|
52
|
+
'EOF',
|
|
53
|
+
'',
|
|
54
|
+
'# 3. Start Claude Code with context',
|
|
55
|
+
'claude-code --project .claude-project.json',
|
|
56
|
+
'',
|
|
57
|
+
'# Alternative: Direct command with context files',
|
|
58
|
+
`claude-code --context "${context.proposalPath}" --context "${context.contextPath}/project.md"`,
|
|
59
|
+
];
|
|
60
|
+
}
|
|
61
|
+
export function generateContextCommands(_context) {
|
|
62
|
+
return [
|
|
63
|
+
'# Context File Commands',
|
|
64
|
+
'# Use these commands to explore the project context:',
|
|
65
|
+
'',
|
|
66
|
+
'# View the current proposal',
|
|
67
|
+
`cat "$GAIT_PROPOSAL_PATH"`,
|
|
68
|
+
'',
|
|
69
|
+
'# View project overview',
|
|
70
|
+
`cat "$GAIT_CONTEXT_PATH/project.md"`,
|
|
71
|
+
'',
|
|
72
|
+
'# View architecture documentation',
|
|
73
|
+
`cat "$GAIT_CONTEXT_PATH/architecture.md"`,
|
|
74
|
+
'',
|
|
75
|
+
'',
|
|
76
|
+
'# List all context files',
|
|
77
|
+
`find "$GAIT_CONTEXT_PATH" -name "*.md" -type f`,
|
|
78
|
+
'',
|
|
79
|
+
'# Search for specific terms in context',
|
|
80
|
+
`grep -r "search_term" "$GAIT_CONTEXT_PATH"`,
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
export function generateBashAliases(_context) {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
export function createInitScript(context) {
|
|
87
|
+
return `#!/bin/bash
|
|
88
|
+
# GAIT Terminal Initialization Script
|
|
89
|
+
# This script is automatically sourced when a terminal session starts
|
|
90
|
+
|
|
91
|
+
# Function to show context help
|
|
92
|
+
gait-context-help() {
|
|
93
|
+
cat << 'GAIT_HELP_EOF'
|
|
94
|
+
${generateWelcomeMessage(context)}
|
|
95
|
+
GAIT_HELP_EOF
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Function to show proposal content
|
|
99
|
+
gait-proposal() {
|
|
100
|
+
if [ -f "$GAIT_PROPOSAL_PATH" ]; then
|
|
101
|
+
echo "\\x1b[36m=== Proposal: $GAIT_PROPOSAL_ID ===\\x1b[0m"
|
|
102
|
+
cat "$GAIT_PROPOSAL_PATH"
|
|
103
|
+
else
|
|
104
|
+
echo "\\x1b[31mProposal file not found: $GAIT_PROPOSAL_PATH\\x1b[0m"
|
|
105
|
+
fi
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Function to list context files
|
|
109
|
+
gait-files() {
|
|
110
|
+
echo "\\x1b[36m=== Context Files ===\\x1b[0m"
|
|
111
|
+
if [ -d "$GAIT_CONTEXT_PATH" ]; then
|
|
112
|
+
find "$GAIT_CONTEXT_PATH" -name "*.md" -type f | sort
|
|
113
|
+
else
|
|
114
|
+
echo "\\x1b[31mContext directory not found: $GAIT_CONTEXT_PATH\\x1b[0m"
|
|
115
|
+
fi
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
# Function to setup Claude Code
|
|
119
|
+
gait-claude() {
|
|
120
|
+
echo "\\x1b[36m=== Claude Code Setup ===\\x1b[0m"
|
|
121
|
+
|
|
122
|
+
# Check if Claude Code is available
|
|
123
|
+
if ! command -v claude-code &> /dev/null; then
|
|
124
|
+
echo "\\x1b[33mClaude Code CLI not found. Please install it first:\\x1b[0m"
|
|
125
|
+
echo "npm install -g @anthropic-ai/claude-code"
|
|
126
|
+
return 1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
# Create project configuration
|
|
130
|
+
cat > .claude-project.json << 'EOF'
|
|
131
|
+
{
|
|
132
|
+
"name": "GAIT Proposal ${context.proposalId}",
|
|
133
|
+
"description": "Working on proposal ${context.proposalId}",
|
|
134
|
+
"context": [
|
|
135
|
+
"${context.proposalPath}",
|
|
136
|
+
"${context.contextPath}/project.md",
|
|
137
|
+
"${context.contextPath}/architecture.md",
|
|
138
|
+
],
|
|
139
|
+
"rules": [
|
|
140
|
+
"Follow the project architecture patterns",
|
|
141
|
+
"Reference the proposal requirements",
|
|
142
|
+
"Maintain consistency with existing code"
|
|
143
|
+
]
|
|
144
|
+
}
|
|
145
|
+
EOF
|
|
146
|
+
|
|
147
|
+
echo "\\x1b[32mClaude Code project configuration created!\\x1b[0m"
|
|
148
|
+
echo "\\x1b[33mRun: claude-code --project .claude-project.json\\x1b[0m"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
# Create helpful alias for help command
|
|
152
|
+
alias gait-help='gait-context-help'
|
|
153
|
+
|
|
154
|
+
# Clear screen first to avoid any leftover content
|
|
155
|
+
clear
|
|
156
|
+
|
|
157
|
+
# Show welcome message
|
|
158
|
+
gait-context-help
|
|
159
|
+
|
|
160
|
+
# Set a custom prompt to show the proposal ID
|
|
161
|
+
# This will be replaced with shell-specific syntax in prepareShellInit
|
|
162
|
+
export PS1="[${context.proposalId}] \\w $ "
|
|
163
|
+
`;
|
|
164
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { TerminalManager } from './terminal-manager.js';
|
|
2
|
+
// Use a type assertion to avoid TypeScript conflicts
|
|
3
|
+
const globalAny = global;
|
|
4
|
+
// Initialize the global singleton if it doesn't exist
|
|
5
|
+
const getOrCreateManager = () => {
|
|
6
|
+
if (!globalAny.gaitTerminalManager) {
|
|
7
|
+
globalAny.gaitTerminalManager = new TerminalManager();
|
|
8
|
+
}
|
|
9
|
+
return globalAny.gaitTerminalManager;
|
|
10
|
+
};
|
|
11
|
+
// Initialize on module load
|
|
12
|
+
getOrCreateManager();
|
|
13
|
+
export function getGlobalTerminalManager() {
|
|
14
|
+
return getOrCreateManager();
|
|
15
|
+
}
|