vibeteam 0.2.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/bin/vibeteam ADDED
@@ -0,0 +1,71 @@
1
+ #!/bin/bash
2
+ #
3
+ # VibTeam CLI Wrapper
4
+ #
5
+ # This wrapper ensures PATH is set up correctly on macOS before running
6
+ # the Node.js CLI. On Linux and properly-configured macOS, it simply
7
+ # passes through to Node.js.
8
+ #
9
+
10
+ # =============================================================================
11
+ # macOS PATH Fix
12
+ # On macOS, the shell environment may not include Homebrew paths.
13
+ # Add them defensively so node, jq, tmux etc. can be found.
14
+ # =============================================================================
15
+ if [[ "$OSTYPE" == "darwin"* ]]; then
16
+ # Apple Silicon Homebrew
17
+ [ -d "/opt/homebrew/bin" ] && export PATH="/opt/homebrew/bin:$PATH"
18
+ # Intel Homebrew
19
+ [ -d "/usr/local/bin" ] && export PATH="/usr/local/bin:$PATH"
20
+ # User local bin (Claude CLI location)
21
+ [ -d "$HOME/.local/bin" ] && export PATH="$HOME/.local/bin:$PATH"
22
+ fi
23
+
24
+ # =============================================================================
25
+ # Find Node.js
26
+ # =============================================================================
27
+ NODE=""
28
+
29
+ # Check PATH first - this respects nvm, fnm, volta, etc.
30
+ NODE=$(command -v node 2>/dev/null)
31
+
32
+ # If not in PATH, check known locations (helps on macOS with limited PATH)
33
+ if [ -z "$NODE" ]; then
34
+ for loc in \
35
+ "/opt/homebrew/bin/node" \
36
+ "/usr/local/bin/node" \
37
+ "$HOME/.local/bin/node" \
38
+ "/usr/bin/node"
39
+ do
40
+ if [ -x "$loc" ]; then
41
+ NODE="$loc"
42
+ break
43
+ fi
44
+ done
45
+ fi
46
+
47
+ # If still not found, give a helpful error
48
+ if [ -z "$NODE" ]; then
49
+ echo "Error: Node.js not found"
50
+ echo ""
51
+ if [[ "$OSTYPE" == "darwin"* ]]; then
52
+ echo "On macOS, install Node.js via Homebrew:"
53
+ echo " brew install node"
54
+ echo ""
55
+ echo "If already installed, ensure Homebrew is in your PATH."
56
+ echo "Add this to your ~/.zshrc:"
57
+ echo " eval \"\$(/opt/homebrew/bin/brew shellenv)\""
58
+ else
59
+ echo "Install Node.js using your package manager:"
60
+ echo " Ubuntu/Debian: sudo apt install nodejs"
61
+ echo " Fedora: sudo dnf install nodejs"
62
+ echo " Arch: sudo pacman -S nodejs"
63
+ fi
64
+ exit 1
65
+ fi
66
+
67
+ # =============================================================================
68
+ # Run the actual CLI
69
+ # =============================================================================
70
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
71
+ exec "$NODE" "$SCRIPT_DIR/cli.js" "$@"
package/data/.gitkeep ADDED
File without changes
@@ -0,0 +1,307 @@
1
+ /**
2
+ * CommitWatcher - Monitor git repositories for new commits on specified branches
3
+ *
4
+ * Similar pattern to GitStatusManager, but focused on detecting new commits
5
+ * to trigger Lead agents.
6
+ */
7
+ import { execFile } from 'child_process';
8
+ import { promisify } from 'util';
9
+
10
+ const execFileAsync = promisify(execFile);
11
+
12
+ // ============================================================================
13
+ // CommitWatcher
14
+ // ============================================================================
15
+ export class CommitWatcher {
16
+ /** Map of projectId -> tracking info */
17
+ trackedProjects = new Map();
18
+
19
+ /** Polling interval reference */
20
+ pollInterval = null;
21
+
22
+ /** Callback when new commit is detected */
23
+ onNewCommit = null;
24
+
25
+ // Configuration
26
+ POLL_INTERVAL_MS = 10000; // Check every 10 seconds
27
+ EXEC_TIMEOUT_MS = 5000; // Timeout for git commands
28
+ FETCH_INTERVAL_MS = 30000; // Fetch from remote every 30 seconds
29
+
30
+ lastFetchTime = 0;
31
+
32
+ constructor() {}
33
+
34
+ /**
35
+ * Set callback for new commit detection
36
+ * @param {Function} handler - (projectId, commitInfo) => void
37
+ */
38
+ setCommitHandler(handler) {
39
+ this.onNewCommit = handler;
40
+ }
41
+
42
+ /**
43
+ * Start tracking a project's branch for new commits
44
+ * @param {string} projectId - Project identifier
45
+ * @param {string} projectPath - Path to git repository
46
+ * @param {string} branch - Branch to watch (default: 'main')
47
+ */
48
+ track(projectId, projectPath, branch = 'main') {
49
+ this.trackedProjects.set(projectId, {
50
+ path: projectPath,
51
+ branch,
52
+ lastCommit: null, // Will be set on first poll
53
+ lastChecked: null,
54
+ });
55
+
56
+ // Immediately check to get baseline
57
+ this.checkProject(projectId);
58
+ }
59
+
60
+ /**
61
+ * Stop tracking a project
62
+ * @param {string} projectId
63
+ */
64
+ untrack(projectId) {
65
+ this.trackedProjects.delete(projectId);
66
+ }
67
+
68
+ /**
69
+ * Check if a project is being tracked
70
+ * @param {string} projectId
71
+ * @returns {boolean}
72
+ */
73
+ isTracking(projectId) {
74
+ return this.trackedProjects.has(projectId);
75
+ }
76
+
77
+ /**
78
+ * Get tracking info for a project
79
+ * @param {string} projectId
80
+ * @returns {Object|null}
81
+ */
82
+ getTrackingInfo(projectId) {
83
+ return this.trackedProjects.get(projectId) || null;
84
+ }
85
+
86
+ /**
87
+ * Start polling for new commits
88
+ */
89
+ start() {
90
+ if (this.pollInterval) return;
91
+
92
+ this.pollInterval = setInterval(() => {
93
+ this.pollAll();
94
+ }, this.POLL_INTERVAL_MS);
95
+
96
+ // Initial poll
97
+ this.pollAll();
98
+ }
99
+
100
+ /**
101
+ * Stop polling
102
+ */
103
+ stop() {
104
+ if (this.pollInterval) {
105
+ clearInterval(this.pollInterval);
106
+ this.pollInterval = null;
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Poll all tracked projects for new commits
112
+ */
113
+ async pollAll() {
114
+ // Periodically fetch from remotes
115
+ const now = Date.now();
116
+ if (now - this.lastFetchTime > this.FETCH_INTERVAL_MS) {
117
+ this.lastFetchTime = now;
118
+ // Fetch in background for all tracked projects
119
+ for (const [projectId, info] of this.trackedProjects) {
120
+ this.execGit(['fetch', '--quiet'], info.path).catch(() => {}); // Silent fail
121
+ }
122
+ }
123
+
124
+ // Check each project
125
+ const promises = Array.from(this.trackedProjects.keys()).map(
126
+ (projectId) => this.checkProject(projectId)
127
+ );
128
+ await Promise.all(promises);
129
+ }
130
+
131
+ /**
132
+ * Check a single project for new commits
133
+ * @param {string} projectId
134
+ */
135
+ async checkProject(projectId) {
136
+ const info = this.trackedProjects.get(projectId);
137
+ if (!info) return;
138
+
139
+ try {
140
+ // Get the latest commit on the tracked branch (from remote)
141
+ const remoteRef = `origin/${info.branch}`;
142
+ const currentSha = await this.getLatestCommit(info.path, remoteRef);
143
+
144
+ info.lastChecked = Date.now();
145
+
146
+ if (!currentSha) return;
147
+
148
+ // First time - just record baseline
149
+ if (!info.lastCommit) {
150
+ info.lastCommit = currentSha;
151
+ return;
152
+ }
153
+
154
+ // Check if commit changed
155
+ if (currentSha !== info.lastCommit) {
156
+ const previousCommit = info.lastCommit;
157
+ info.lastCommit = currentSha;
158
+
159
+ // Get commit details
160
+ const commitInfo = await this.getCommitInfo(info.path, currentSha);
161
+
162
+ if (commitInfo && this.onNewCommit) {
163
+ this.onNewCommit(projectId, commitInfo);
164
+ }
165
+ }
166
+ } catch (err) {
167
+ // Silent fail - project might not have remote configured
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Get the latest commit SHA for a ref
173
+ * @param {string} path - Repository path
174
+ * @param {string} ref - Git ref (e.g., 'origin/main')
175
+ * @returns {Promise<string|null>}
176
+ */
177
+ async getLatestCommit(path, ref) {
178
+ try {
179
+ const result = await this.execGit(
180
+ ['log', '-1', '--format=%H', ref],
181
+ path
182
+ );
183
+ return result.trim() || null;
184
+ } catch {
185
+ return null;
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Get detailed commit information
191
+ * @param {string} path - Repository path
192
+ * @param {string} sha - Commit SHA
193
+ * @returns {Promise<Object|null>}
194
+ */
195
+ async getCommitInfo(path, sha) {
196
+ try {
197
+ // Get commit details
198
+ const format = '%H|||%h|||%s|||%an|||%at';
199
+ const logResult = await this.execGit(
200
+ ['show', '--format=' + format, '--stat', '--no-patch', sha],
201
+ path
202
+ );
203
+
204
+ const lines = logResult.trim().split('\n');
205
+ if (lines.length === 0) return null;
206
+
207
+ const [fullSha, shortSha, message, author, timestamp] = lines[0].split('|||');
208
+
209
+ // Get diff stats
210
+ const diffResult = await this.execGit(
211
+ ['diff', '--shortstat', `${sha}^..${sha}`],
212
+ path
213
+ ).catch(() => '');
214
+
215
+ let additions = 0;
216
+ let deletions = 0;
217
+ const addMatch = diffResult.match(/(\d+) insertion/);
218
+ const delMatch = diffResult.match(/(\d+) deletion/);
219
+ if (addMatch) additions = parseInt(addMatch[1], 10);
220
+ if (delMatch) deletions = parseInt(delMatch[1], 10);
221
+
222
+ // Get list of changed files
223
+ const filesResult = await this.execGit(
224
+ ['diff', '--name-only', `${sha}^..${sha}`],
225
+ path
226
+ ).catch(() => '');
227
+
228
+ const filesChanged = filesResult.trim().split('\n').filter(Boolean);
229
+
230
+ // Get the branch this commit is on
231
+ const branchResult = await this.execGit(
232
+ ['branch', '-r', '--contains', sha],
233
+ path
234
+ ).catch(() => '');
235
+
236
+ // Extract branch name (first one if multiple)
237
+ let branch = 'main';
238
+ const branchMatch = branchResult.match(/origin\/(\S+)/);
239
+ if (branchMatch) {
240
+ branch = branchMatch[1];
241
+ }
242
+
243
+ return {
244
+ sha: fullSha,
245
+ shortSha,
246
+ message,
247
+ author,
248
+ timestamp: parseInt(timestamp, 10) * 1000, // Convert to ms
249
+ branch,
250
+ filesChanged,
251
+ additions,
252
+ deletions,
253
+ };
254
+ } catch {
255
+ return null;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Force check a project (useful after manual trigger)
261
+ * @param {string} projectId
262
+ * @returns {Promise<Object|null>} - Returns commit info if found
263
+ */
264
+ async forceCheck(projectId) {
265
+ const info = this.trackedProjects.get(projectId);
266
+ if (!info) return null;
267
+
268
+ try {
269
+ // Fetch first
270
+ await this.execGit(['fetch', '--quiet'], info.path);
271
+
272
+ // Get latest commit
273
+ const remoteRef = `origin/${info.branch}`;
274
+ const currentSha = await this.getLatestCommit(info.path, remoteRef);
275
+
276
+ if (!currentSha) return null;
277
+
278
+ return this.getCommitInfo(info.path, currentSha);
279
+ } catch {
280
+ return null;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Get commit info for a specific SHA in a project
286
+ * @param {string} path - Repository path
287
+ * @param {string} sha - Commit SHA
288
+ * @returns {Promise<Object|null>}
289
+ */
290
+ async getCommitInfoForPath(path, sha) {
291
+ return this.getCommitInfo(path, sha);
292
+ }
293
+
294
+ /**
295
+ * Execute a git command in a directory
296
+ * @param {string[]} args - Git command arguments
297
+ * @param {string} cwd - Working directory
298
+ * @returns {Promise<string>}
299
+ */
300
+ async execGit(args, cwd) {
301
+ const { stdout } = await execFileAsync('git', args, {
302
+ cwd,
303
+ timeout: this.EXEC_TIMEOUT_MS,
304
+ });
305
+ return stdout;
306
+ }
307
+ }