• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

apowers313 / aiforge / 20962792399

13 Jan 2026 03:39PM UTC coverage: 84.707%. First build
20962792399

push

github

apowers313
feat: initial commit

787 of 905 branches covered (86.96%)

Branch coverage included in aggregate %.

4248 of 5039 new or added lines in 70 files covered. (84.3%)

4248 of 5039 relevant lines covered (84.3%)

13.11 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.03
/src/server/services/shell/ShellService.ts
1
/**
2
 * ShellService - Shell business logic
3
 */
4
import { randomUUID } from 'node:crypto';
1✔
5
import type { Shell } from '@shared/types/index.js';
6
import type { ShellStore } from '../../storage/stores/ShellStore.js';
7
import type { ProjectStore } from '../../storage/stores/ProjectStore.js';
8
import { PtyPool, type ShellStoreInterface } from '../pty/index.js';
9

10
export interface ShellServiceOptions {
11
  shellStore: ShellStore;
12
  projectStore: ProjectStore;
13
  ptyPool?: PtyPool;
14
}
15

16
export class ShellService {
1✔
17
  private readonly shellStore: ShellStore;
8✔
18
  private readonly projectStore: ProjectStore;
8✔
19
  private readonly ptyPool: PtyPool | null;
8✔
20

21
  constructor(options: ShellServiceOptions) {
8✔
22
    this.shellStore = options.shellStore;
8✔
23
    this.projectStore = options.projectStore;
8✔
24
    this.ptyPool = options.ptyPool ?? null;
8✔
25

26
    // Set up event handlers if ptyPool is provided
27
    if (this.ptyPool) {
8!
NEW
28
      this.ptyPool.on('session:exited', (shellId: string, exitCode: number) => {
×
NEW
29
        void this._handleSessionExit(shellId, exitCode);
×
NEW
30
      });
×
NEW
31
    }
×
32
  }
8✔
33

34
  /**
35
   * Handle PTY session exit
36
   */
37
  private async _handleSessionExit(shellId: string, _exitCode: number): Promise<void> {
8✔
NEW
38
    await this.shellStore.update(shellId, {
×
NEW
39
      status: 'inactive',
×
NEW
40
      pid: null,
×
NEW
41
    });
×
NEW
42
  }
×
43

44
  /**
45
   * Get all shells for a project
46
   */
47
  async getByProjectId(projectId: string): Promise<Shell[]> {
8✔
48
    return this.shellStore.getByProjectId(projectId);
1✔
49
  }
1✔
50

51
  /**
52
   * Get a shell by ID
53
   */
54
  async getById(id: string): Promise<Shell | null> {
8✔
NEW
55
    return this.shellStore.getById(id);
×
NEW
56
  }
×
57

58
  /**
59
   * Create a new shell for a project
60
   */
61
  async create(projectId: string, name?: string): Promise<Shell> {
8✔
62
    // Verify project exists
63
    const project = await this.projectStore.getById(projectId);
7✔
64
    if (!project) {
7✔
65
      throw new Error('Project not found');
1✔
66
    }
1✔
67

68
    // Auto-generate name if not provided
69
    let shellName = name;
6✔
70
    if (!shellName) {
7✔
71
      const shellNumber = await this.shellStore.getNextShellNumber();
1✔
72
      shellName = `shell-${String(shellNumber)}`;
1✔
73
    }
1✔
74

75
    const now = new Date().toISOString();
6✔
76
    const shell: Shell = {
6✔
77
      id: randomUUID(),
6✔
78
      projectId,
6✔
79
      name: shellName,
6✔
80
      cwd: project.path,
6✔
81
      status: 'inactive',
6✔
82
      pid: null,
6✔
83
      createdAt: now,
6✔
84
      updatedAt: now,
6✔
85
    };
6✔
86

87
    await this.shellStore.create(shell);
6✔
88
    return shell;
6✔
89
  }
7✔
90

91
  /**
92
   * Update a shell's properties
93
   */
94
  async update(id: string, updates: Partial<Pick<Shell, 'name' | 'status' | 'pid' | 'cwd'>>): Promise<Shell | null> {
8✔
95
    return this.shellStore.update(id, updates);
2✔
96
  }
2✔
97

98
  /**
99
   * Delete a shell
100
   */
101
  async delete(id: string): Promise<boolean> {
8✔
102
    // Kill the PTY session if running
103
    if (this.ptyPool) {
2!
NEW
104
      this.ptyPool.kill(id);
×
NEW
105
    }
×
106
    return this.shellStore.delete(id);
2✔
107
  }
2✔
108

109
  /**
110
   * Start a shell (spawn PTY process)
111
   */
112
  async start(shellId: string): Promise<Shell> {
8✔
113
    if (!this.ptyPool) {
1✔
114
      throw new Error('PTY pool not configured');
1✔
115
    }
1!
116

NEW
117
    const shell = await this.shellStore.getById(shellId);
×
NEW
118
    if (!shell) {
×
NEW
119
      throw new Error('Shell not found');
×
NEW
120
    }
×
121

122
    // Check if already running
NEW
123
    if (this.ptyPool.get(shellId)) {
×
NEW
124
      return shell;
×
NEW
125
    }
×
126

127
    // Load scrollback from disk first (for replay on client attach)
NEW
128
    await this.ptyPool.loadScrollback(shellId);
×
129

130
    // Add restart separator to scrollback if there's existing content
NEW
131
    const scrollbackStore = this.ptyPool.manager.scrollbackStore;
×
NEW
132
    if (scrollbackStore) {
×
NEW
133
      const existingEntries = scrollbackStore.getFromMemory(shellId);
×
NEW
134
      if (existingEntries.length > 0) {
×
NEW
135
        scrollbackStore.append(shellId, 'output', '\r\n\r\n--- shell restarted ---\r\n\r\n');
×
NEW
136
      }
×
NEW
137
    }
×
138

139
    // Spawn PTY session
NEW
140
    const session = this.ptyPool.spawn(shellId, {
×
NEW
141
      cwd: shell.cwd,
×
NEW
142
    });
×
143

144
    // Update shell status
NEW
145
    const updated = await this.shellStore.update(shellId, {
×
NEW
146
      status: 'active',
×
NEW
147
      pid: session.pid,
×
NEW
148
    });
×
149

NEW
150
    return updated ?? shell;
×
151
  }
1✔
152

153
  /**
154
   * Stop a shell (kill PTY process)
155
   */
156
  async stop(shellId: string): Promise<Shell | null> {
8✔
157
    if (!this.ptyPool) {
1✔
158
      throw new Error('PTY pool not configured');
1✔
159
    }
1!
160

NEW
161
    this.ptyPool.kill(shellId);
×
162

NEW
163
    return this.shellStore.update(shellId, {
×
NEW
164
      status: 'inactive',
×
NEW
165
      pid: null,
×
NEW
166
    });
×
167
  }
1✔
168

169
  /**
170
   * Get the PTY pool (for WebSocket handler access)
171
   */
172
  getPtyPool(): PtyPool | null {
8✔
NEW
173
    return this.ptyPool;
×
NEW
174
  }
×
175

176
  /**
177
   * Clean up orphaned sessions
178
   */
179
  async cleanupOrphans(): Promise<void> {
8✔
NEW
180
    if (this.ptyPool) {
×
NEW
181
      await this.ptyPool.cleanupOrphans(this.shellStore as unknown as ShellStoreInterface);
×
NEW
182
    }
×
NEW
183
  }
×
184

185
  /**
186
   * Shutdown all PTY sessions
187
   */
188
  shutdown(): void {
8✔
NEW
189
    if (this.ptyPool) {
×
NEW
190
      this.ptyPool.shutdown();
×
NEW
191
    }
×
NEW
192
  }
×
193
}
8✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc