@mastra/agent-builder 0.0.0-experimental-agent-builder-20250815195917 → 0.0.1-alpha.1

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/src/utils.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { exec as execNodejs, spawn as nodeSpawn } from 'child_process';
2
- import { existsSync } from 'fs';
1
+ import { exec as execNodejs, execFile as execFileNodejs, spawn as nodeSpawn } from 'child_process';
2
+ import type { SpawnOptions } from 'child_process';
3
+ import { existsSync, readFileSync } from 'fs';
3
4
  import { copyFile } from 'fs/promises';
4
5
  import { createRequire } from 'module';
5
6
  import { dirname, basename, extname, resolve } from 'path';
@@ -8,34 +9,197 @@ import { UNIT_KINDS } from './types';
8
9
  import type { UnitKind } from './types';
9
10
 
10
11
  export const exec = promisify(execNodejs);
12
+ export const execFile = promisify(execFileNodejs);
13
+
14
+ // Helper function to detect if we're in a workspace subfolder
15
+ function isInWorkspaceSubfolder(cwd: string): boolean {
16
+ try {
17
+ // First, check if current directory has package.json (it's a package)
18
+ const currentPackageJson = resolve(cwd, 'package.json');
19
+ if (!existsSync(currentPackageJson)) {
20
+ return false; // Not a package, so not a workspace subfolder
21
+ }
22
+
23
+ // Walk up the directory tree looking for workspace indicators
24
+ let currentDir = cwd;
25
+ let previousDir = '';
26
+
27
+ // Keep going up until we reach the filesystem root or stop making progress
28
+ while (currentDir !== previousDir && currentDir !== '/') {
29
+ previousDir = currentDir;
30
+ currentDir = dirname(currentDir);
31
+
32
+ // Skip if we're back at the original directory
33
+ if (currentDir === cwd) {
34
+ continue;
35
+ }
36
+
37
+ console.log(`Checking for workspace indicators in: ${currentDir}`);
38
+
39
+ // Check for pnpm workspace
40
+ if (existsSync(resolve(currentDir, 'pnpm-workspace.yaml'))) {
41
+ return true;
42
+ }
43
+
44
+ // Check for npm/yarn workspaces in package.json
45
+ const parentPackageJson = resolve(currentDir, 'package.json');
46
+ if (existsSync(parentPackageJson)) {
47
+ try {
48
+ const parentPkg = JSON.parse(readFileSync(parentPackageJson, 'utf-8'));
49
+ if (parentPkg.workspaces) {
50
+ return true; // Found workspace config
51
+ }
52
+ } catch {
53
+ // Ignore JSON parse errors
54
+ }
55
+ }
56
+
57
+ // Check for lerna
58
+ if (existsSync(resolve(currentDir, 'lerna.json'))) {
59
+ return true;
60
+ }
61
+ }
62
+
63
+ return false;
64
+ } catch (error) {
65
+ console.log(`Error in workspace detection: ${error}`);
66
+ return false; // Default to false on any error
67
+ }
68
+ }
11
69
 
12
70
  export function spawn(command: string, args: string[], options: any) {
13
71
  return new Promise((resolve, reject) => {
14
72
  const childProcess = nodeSpawn(command, args, {
15
- // stdio: 'inherit',
73
+ stdio: 'inherit', // Enable proper stdio handling
16
74
  ...options,
17
75
  });
18
76
  childProcess.on('error', error => {
19
77
  reject(error);
20
78
  });
79
+ childProcess.on('close', code => {
80
+ if (code === 0) {
81
+ resolve(void 0);
82
+ } else {
83
+ reject(new Error(`Command failed with exit code ${code}`));
84
+ }
85
+ });
86
+ });
87
+ }
88
+
89
+ // --- Git environment probes ---
90
+ export async function isGitInstalled(): Promise<boolean> {
91
+ try {
92
+ await spawnWithOutput('git', ['--version'], {});
93
+ return true;
94
+ } catch {
95
+ return false;
96
+ }
97
+ }
98
+
99
+ export async function isInsideGitRepo(cwd: string): Promise<boolean> {
100
+ try {
101
+ if (!(await isGitInstalled())) return false;
102
+ const { stdout } = await spawnWithOutput('git', ['rev-parse', '--is-inside-work-tree'], { cwd });
103
+ return stdout.trim() === 'true';
104
+ } catch {
105
+ return false;
106
+ }
107
+ }
108
+
109
+ // Variant of spawn that captures stdout and stderr
110
+ export function spawnWithOutput(
111
+ command: string,
112
+ args: string[],
113
+ options: SpawnOptions,
114
+ ): Promise<{ stdout: string; stderr: string; code: number }> {
115
+ return new Promise((resolvePromise, rejectPromise) => {
116
+ const childProcess = nodeSpawn(command, args, {
117
+ ...options,
118
+ });
119
+ let stdout = '';
21
120
  let stderr = '';
22
- childProcess.stderr?.on('data', message => {
23
- stderr += message;
121
+ childProcess.on('error', error => {
122
+ rejectPromise(error);
123
+ });
124
+ childProcess.stdout?.on('data', chunk => {
125
+ process.stdout.write(chunk);
126
+ stdout += chunk?.toString?.() ?? String(chunk);
127
+ });
128
+ childProcess.stderr?.on('data', chunk => {
129
+ stderr += chunk?.toString?.() ?? String(chunk);
130
+ process.stderr.write(chunk);
24
131
  });
25
132
  childProcess.on('close', code => {
26
133
  if (code === 0) {
27
- resolve(void 0);
134
+ resolvePromise({ stdout, stderr, code: code ?? 0 });
28
135
  } else {
29
- reject(new Error(stderr));
136
+ const err = new Error(stderr || `Command failed: ${command} ${args.join(' ')}`);
137
+ // @ts-expect-error augment
138
+ err.code = code;
139
+ rejectPromise(err);
30
140
  }
31
141
  });
32
142
  });
33
143
  }
34
144
 
35
145
  export async function spawnSWPM(cwd: string, command: string, packageNames: string[]) {
36
- await spawn(createRequire(import.meta.filename).resolve('swpm'), [command, ...packageNames], {
37
- cwd,
38
- });
146
+ // 1) Try local swpm module resolution/execution
147
+ try {
148
+ console.log('Running install command with swpm');
149
+ const swpmPath = createRequire(import.meta.filename).resolve('swpm');
150
+ await spawn(swpmPath, [command, ...packageNames], { cwd });
151
+ return;
152
+ } catch (e) {
153
+ console.log('Failed to run install command with swpm', e);
154
+ // ignore and try fallbacks
155
+ }
156
+
157
+ // 2) Fallback to native package manager based on lock files
158
+ try {
159
+ // Detect package manager from lock files
160
+ let packageManager: string;
161
+
162
+ if (existsSync(resolve(cwd, 'pnpm-lock.yaml'))) {
163
+ packageManager = 'pnpm';
164
+ } else if (existsSync(resolve(cwd, 'yarn.lock'))) {
165
+ packageManager = 'yarn';
166
+ } else {
167
+ packageManager = 'npm';
168
+ }
169
+
170
+ // Normalize command
171
+ let nativeCommand = command === 'add' ? 'add' : command === 'install' ? 'install' : command;
172
+
173
+ // Build args with non-interactive flags for install commands
174
+ const args = [nativeCommand];
175
+ if (nativeCommand === 'install') {
176
+ const inWorkspace = isInWorkspaceSubfolder(cwd);
177
+ if (packageManager === 'pnpm') {
178
+ args.push('--force'); // pnpm install --force
179
+
180
+ // Check if we're in a workspace subfolder
181
+ if (inWorkspace) {
182
+ args.push('--ignore-workspace');
183
+ }
184
+ } else if (packageManager === 'npm') {
185
+ args.push('--yes'); // npm install --yes
186
+
187
+ // Check if we're in a workspace subfolder
188
+ if (inWorkspace) {
189
+ args.push('--ignore-workspaces');
190
+ }
191
+ }
192
+ }
193
+ args.push(...packageNames);
194
+
195
+ console.log(`Falling back to ${packageManager} ${args.join(' ')}`);
196
+ await spawn(packageManager, args, { cwd });
197
+ return;
198
+ } catch (e) {
199
+ console.log(`Failed to run install command with native package manager: ${e}`);
200
+ }
201
+
202
+ throw new Error(`Failed to run install command with swpm and native package managers`);
39
203
  }
40
204
 
41
205
  // Utility functions
@@ -88,9 +252,11 @@ export async function getMastraTemplate(slug: string) {
88
252
  // Git commit tracking utility
89
253
  export async function logGitState(targetPath: string, label: string): Promise<void> {
90
254
  try {
91
- const gitStatusResult = await exec('git status --porcelain', { cwd: targetPath });
92
- const gitLogResult = await exec('git log --oneline -3', { cwd: targetPath });
93
- const gitCountResult = await exec('git rev-list --count HEAD', { cwd: targetPath });
255
+ // Skip if not a git repo
256
+ if (!(await isInsideGitRepo(targetPath))) return;
257
+ const gitStatusResult = await git(targetPath, 'status', '--porcelain');
258
+ const gitLogResult = await git(targetPath, 'log', '--oneline', '-3');
259
+ const gitCountResult = await git(targetPath, 'rev-list', '--count', 'HEAD');
94
260
 
95
261
  console.log(`📊 Git state ${label}:`);
96
262
  console.log('Status:', gitStatusResult.stdout.trim() || 'Clean working directory');
@@ -101,6 +267,116 @@ export async function logGitState(targetPath: string, label: string): Promise<vo
101
267
  }
102
268
  }
103
269
 
270
+ // Generic git runner that captures stdout/stderr
271
+ export async function git(cwd: string, ...args: string[]): Promise<{ stdout: string; stderr: string }> {
272
+ const { stdout, stderr } = await spawnWithOutput('git', args, { cwd });
273
+ return { stdout: stdout ?? '', stderr: stderr ?? '' };
274
+ }
275
+
276
+ // Common git helpers
277
+ export async function gitClone(repo: string, destDir: string, cwd?: string) {
278
+ await git(cwd ?? process.cwd(), 'clone', repo, destDir);
279
+ }
280
+
281
+ export async function gitCheckoutRef(cwd: string, ref: string) {
282
+ if (!(await isInsideGitRepo(cwd))) return;
283
+ await git(cwd, 'checkout', ref);
284
+ }
285
+
286
+ export async function gitRevParse(cwd: string, rev: string): Promise<string> {
287
+ if (!(await isInsideGitRepo(cwd))) return '';
288
+ const { stdout } = await git(cwd, 'rev-parse', rev);
289
+ return stdout.trim();
290
+ }
291
+
292
+ export async function gitAddFiles(cwd: string, files: string[]) {
293
+ if (!files || files.length === 0) return;
294
+ if (!(await isInsideGitRepo(cwd))) return;
295
+ await git(cwd, 'add', ...files);
296
+ }
297
+
298
+ export async function gitAddAll(cwd: string) {
299
+ if (!(await isInsideGitRepo(cwd))) return;
300
+ await git(cwd, 'add', '.');
301
+ }
302
+
303
+ export async function gitHasStagedChanges(cwd: string): Promise<boolean> {
304
+ if (!(await isInsideGitRepo(cwd))) return false;
305
+ const { stdout } = await git(cwd, 'diff', '--cached', '--name-only');
306
+ return stdout.trim().length > 0;
307
+ }
308
+
309
+ export async function gitCommit(
310
+ cwd: string,
311
+ message: string,
312
+ opts?: { allowEmpty?: boolean; skipIfNoStaged?: boolean },
313
+ ): Promise<boolean> {
314
+ try {
315
+ if (!(await isInsideGitRepo(cwd))) return false;
316
+ if (opts?.skipIfNoStaged) {
317
+ const has = await gitHasStagedChanges(cwd);
318
+ if (!has) return false;
319
+ }
320
+ const args = ['commit', '-m', message];
321
+ if (opts?.allowEmpty) args.push('--allow-empty');
322
+ await git(cwd, ...args);
323
+ return true;
324
+ } catch (e) {
325
+ const msg = e instanceof Error ? e.message : String(e);
326
+ if (/nothing to commit/i.test(msg) || /no changes added to commit/i.test(msg)) {
327
+ return false;
328
+ }
329
+ throw e;
330
+ }
331
+ }
332
+
333
+ export async function gitAddAndCommit(
334
+ cwd: string,
335
+ message: string,
336
+ files?: string[],
337
+ opts?: { allowEmpty?: boolean; skipIfNoStaged?: boolean },
338
+ ): Promise<boolean> {
339
+ try {
340
+ if (!(await isInsideGitRepo(cwd))) return false;
341
+ if (files && files.length > 0) {
342
+ await gitAddFiles(cwd, files);
343
+ } else {
344
+ await gitAddAll(cwd);
345
+ }
346
+ return gitCommit(cwd, message, opts);
347
+ } catch (e) {
348
+ console.error(`Failed to add and commit files: ${e instanceof Error ? e.message : String(e)}`);
349
+ return false;
350
+ }
351
+ }
352
+
353
+ export async function gitCheckoutBranch(branchName: string, targetPath: string) {
354
+ try {
355
+ if (!(await isInsideGitRepo(targetPath))) return;
356
+ // Try to create new branch using centralized git runner
357
+ await git(targetPath, 'checkout', '-b', branchName);
358
+ console.log(`Created new branch: ${branchName}`);
359
+ } catch (error) {
360
+ // If branch exists, check if we can switch to it or create a unique name
361
+ const errorStr = error instanceof Error ? error.message : String(error);
362
+ if (errorStr.includes('already exists')) {
363
+ try {
364
+ // Try to switch to existing branch
365
+ await git(targetPath, 'checkout', branchName);
366
+ console.log(`Switched to existing branch: ${branchName}`);
367
+ } catch {
368
+ // If can't switch, create a unique branch name
369
+ const timestamp = Date.now().toString().slice(-6);
370
+ const uniqueBranchName = `${branchName}-${timestamp}`;
371
+ await git(targetPath, 'checkout', '-b', uniqueBranchName);
372
+ console.log(`Created unique branch: ${uniqueBranchName}`);
373
+ }
374
+ } else {
375
+ throw error; // Re-throw if it's a different error
376
+ }
377
+ }
378
+ }
379
+
104
380
  // File conflict resolution utilities (for future use)
105
381
  export async function backupAndReplaceFile(sourceFile: string, targetFile: string): Promise<void> {
106
382
  // Create backup of existing file