@mastra/factory 0.2.1 → 0.2.2-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.
Files changed (59) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/dist/auth.d.ts +2 -2
  3. package/dist/auth.d.ts.map +1 -1
  4. package/dist/auth.js +22 -2
  5. package/dist/auth.js.map +1 -1
  6. package/dist/factory.d.ts.map +1 -1
  7. package/dist/factory.js +411 -244
  8. package/dist/factory.js.map +1 -1
  9. package/dist/index.js +411 -244
  10. package/dist/index.js.map +1 -1
  11. package/dist/integrations/github/integration.js +85 -37
  12. package/dist/integrations/github/integration.js.map +1 -1
  13. package/dist/integrations/github/routes.d.ts.map +1 -1
  14. package/dist/integrations/github/routes.js +85 -37
  15. package/dist/integrations/github/routes.js.map +1 -1
  16. package/dist/integrations/github/sandbox.d.ts +2 -0
  17. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  18. package/dist/integrations/github/sandbox.js +28 -8
  19. package/dist/integrations/github/sandbox.js.map +1 -1
  20. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  21. package/dist/integrations/platform/github/integration.js +119 -39
  22. package/dist/integrations/platform/github/integration.js.map +1 -1
  23. package/dist/routes/config.d.ts.map +1 -1
  24. package/dist/routes/config.js +1 -4
  25. package/dist/routes/config.js.map +1 -1
  26. package/dist/routes/surface.js +1 -4
  27. package/dist/routes/surface.js.map +1 -1
  28. package/dist/routes/work-items.js.map +1 -1
  29. package/dist/rules/binding-context.js.map +1 -1
  30. package/dist/rules/dispatcher.d.ts.map +1 -1
  31. package/dist/rules/dispatcher.js +7 -1
  32. package/dist/rules/dispatcher.js.map +1 -1
  33. package/dist/rules/processor.js.map +1 -1
  34. package/dist/rules/tools.js.map +1 -1
  35. package/dist/spa-static.d.ts.map +1 -1
  36. package/dist/spa-static.js +1 -0
  37. package/dist/spa-static.js.map +1 -1
  38. package/dist/storage/domains/source-control/base.d.ts +9 -0
  39. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  40. package/dist/storage/domains/source-control/base.js +4 -0
  41. package/dist/storage/domains/source-control/base.js.map +1 -1
  42. package/dist/storage/domains/source-control/inmemory.d.ts +4 -0
  43. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  44. package/dist/storage/domains/source-control/inmemory.js +4 -0
  45. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  46. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  47. package/dist/storage/domains/work-items/base.js +20 -4
  48. package/dist/storage/domains/work-items/base.js.map +1 -1
  49. package/dist/storage/domains/work-items/metrics.js.map +1 -1
  50. package/dist/timing.d.ts +15 -0
  51. package/dist/timing.d.ts.map +1 -0
  52. package/dist/timing.js +27 -0
  53. package/dist/timing.js.map +1 -0
  54. package/dist/workspace.d.ts +2 -3
  55. package/dist/workspace.d.ts.map +1 -1
  56. package/dist/workspace.js +104 -65
  57. package/dist/workspace.js.map +1 -1
  58. package/factory-skills/factory-review/SKILL.md +9 -2
  59. package/package.json +7 -7
@@ -113,8 +113,23 @@ async function teardownProjectSandbox(options) {
113
113
  function shellQuote(value) {
114
114
  return `'` + value.split(`'`).join(`'\\''`) + `'`;
115
115
  }
116
- async function sh(sandbox, script) {
117
- return sandbox.executeCommand("sh", ["-c", script]);
116
+ var DEFAULT_COMMAND_TIMEOUT_MS = 15 * 6e4;
117
+ var CHECKOUT_COMMAND_TIMEOUT_MS = 5 * 6e4;
118
+ async function sh(sandbox, script, options = {}) {
119
+ const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
120
+ let timer;
121
+ const hangGuard = new Promise((_, reject) => {
122
+ timer = setTimeout(() => {
123
+ const phase = options.phase ? ` during ${options.phase}` : "";
124
+ reject(new Error(`Sandbox command timed out after ${Math.round(timeoutMs / 1e3)}s${phase}.`));
125
+ }, timeoutMs);
126
+ timer.unref?.();
127
+ });
128
+ try {
129
+ return await Promise.race([sandbox.executeCommand("sh", ["-c", script], { timeout: timeoutMs }), hangGuard]);
130
+ } finally {
131
+ clearTimeout(timer);
132
+ }
118
133
  }
119
134
  var MaterializeError = class extends Error {
120
135
  constructor(message, code) {
@@ -151,7 +166,8 @@ async function materializeRepo(options) {
151
166
  );
152
167
  }
153
168
  const authUrl = tokenUrl(repo, token);
154
- const alreadyMaterialized = Boolean(sandboxRow.materializedAt) || await hasExistingCheckout(sandbox, workdir, repo);
169
+ const alreadyMaterialized = await hasExistingCheckout(sandbox, workdir, repo);
170
+ let succeeded = false;
155
171
  try {
156
172
  if (!alreadyMaterialized) {
157
173
  reportProgress(onProgress, {
@@ -160,7 +176,8 @@ async function materializeRepo(options) {
160
176
  });
161
177
  const clone = await sh(
162
178
  sandbox,
163
- `git clone --depth=1 --single-branch --branch ${shellQuote(repoInfo.defaultBranch)} ${shellQuote(authUrl)} ${shellQuote(workdir)}`
179
+ `git clone --depth=1 --single-branch --branch ${shellQuote(repoInfo.defaultBranch)} ${shellQuote(authUrl)} ${shellQuote(workdir)}`,
180
+ { phase: "repository clone" }
164
181
  );
165
182
  if (clone.exitCode !== 0) {
166
183
  throw classifyGitFailure(clone, "clone-failed");
@@ -171,13 +188,14 @@ async function materializeRepo(options) {
171
188
  if (setUrl.exitCode !== 0) {
172
189
  throw new MaterializeError(`Failed to set git remote: ${setUrl.stderr}`, "pull-failed");
173
190
  }
174
- const pull = await sh(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`);
191
+ const pull = await sh(sandbox, `git -C ${shellQuote(workdir)} pull --ff-only`, { phase: "repository pull" });
175
192
  if (pull.exitCode !== 0) {
176
193
  throw classifyGitFailure(pull, "pull-failed");
177
194
  }
178
195
  }
196
+ succeeded = true;
179
197
  } finally {
180
- await scrubRemote(sandbox, workdir, repo, alreadyMaterialized);
198
+ await scrubRemote(sandbox, workdir, repo, succeeded);
181
199
  }
182
200
  reportProgress(onProgress, { phase: "finalizing", message: "Finalizing workspace\u2026" });
183
201
  await storage.markMaterialized({ id: sandboxRow.id });
@@ -1076,43 +1094,56 @@ function buildGithubRoutes(options) {
1076
1094
  handler: async (c) => {
1077
1095
  const resolved = await resolveOrgTenant(loose(c), auth);
1078
1096
  if ("response" in resolved) return resolved.response;
1079
- const installs = await github.sourceControlStorage.installations.list({ orgId: resolved.tenant.orgId });
1097
+ const orgId = resolved.tenant.orgId;
1098
+ const installs = await github.sourceControlStorage.installations.list({ orgId });
1080
1099
  const query = (c.req.query("q") ?? "").toLowerCase();
1081
- const repos = [];
1100
+ const listed = await Promise.all(
1101
+ installs.map(async (inst) => {
1102
+ try {
1103
+ return { inst, list: await github.listInstallationRepos(Number(inst.externalId)) };
1104
+ } catch (err) {
1105
+ if (err.status !== 404) throw err;
1106
+ console.error(`[Mastra Factory] pruning stale GitHub installation ${inst.externalId} (404 from GitHub)`);
1107
+ await github.sourceControlStorage.installations.delete({ orgId, id: inst.id });
1108
+ return { inst, list: [] };
1109
+ }
1110
+ })
1111
+ );
1112
+ const matches = [];
1082
1113
  const seenRepositoryIds = /* @__PURE__ */ new Set();
1083
- for (const inst of installs) {
1084
- let list;
1085
- try {
1086
- list = await github.listInstallationRepos(Number(inst.externalId));
1087
- } catch (err) {
1088
- if (err.status !== 404) throw err;
1089
- console.error(`[Mastra Factory] pruning stale GitHub installation ${inst.externalId} (404 from GitHub)`);
1090
- await github.sourceControlStorage.installations.delete({ orgId: resolved.tenant.orgId, id: inst.id });
1091
- continue;
1092
- }
1114
+ for (const { inst, list } of listed) {
1093
1115
  for (const repo of list) {
1094
1116
  if (query && !repo.fullName.toLowerCase().includes(query)) continue;
1095
1117
  if (seenRepositoryIds.has(repo.id)) continue;
1096
1118
  seenRepositoryIds.add(repo.id);
1097
- const repository = await github.sourceControlStorage.repositories.upsert({
1098
- orgId: resolved.tenant.orgId,
1099
- input: {
1100
- installationId: inst.id,
1101
- externalId: repo.id.toString(),
1102
- slug: repo.fullName,
1103
- defaultBranch: isValidGitRef2(repo.defaultBranch) ? repo.defaultBranch : "main",
1104
- providerMetadata: { private: repo.private, owner: repo.owner }
1105
- }
1106
- });
1107
- repos.push({
1108
- ...repo,
1109
- installationStorageId: inst.id,
1110
- repositoryStorageId: repository.id,
1111
- sandboxProvider: fleet.provider,
1112
- sandboxWorkdir: fleet.computeWorkdir(repo.fullName)
1113
- });
1119
+ matches.push({ inst, repo });
1114
1120
  }
1115
1121
  }
1122
+ const repos = new Array(matches.length);
1123
+ const upsertConcurrency = 10;
1124
+ for (let start = 0; start < matches.length; start += upsertConcurrency) {
1125
+ await Promise.all(
1126
+ matches.slice(start, start + upsertConcurrency).map(async ({ inst, repo }, offset) => {
1127
+ const repository = await github.sourceControlStorage.repositories.upsert({
1128
+ orgId,
1129
+ input: {
1130
+ installationId: inst.id,
1131
+ externalId: repo.id.toString(),
1132
+ slug: repo.fullName,
1133
+ defaultBranch: isValidGitRef2(repo.defaultBranch) ? repo.defaultBranch : "main",
1134
+ providerMetadata: { private: repo.private, owner: repo.owner }
1135
+ }
1136
+ });
1137
+ repos[start + offset] = {
1138
+ ...repo,
1139
+ installationStorageId: inst.id,
1140
+ repositoryStorageId: repository.id,
1141
+ sandboxProvider: fleet.provider,
1142
+ sandboxWorkdir: fleet.computeWorkdir(repo.fullName)
1143
+ };
1144
+ })
1145
+ );
1146
+ }
1116
1147
  return c.json({ repos });
1117
1148
  }
1118
1149
  })
@@ -1443,8 +1474,25 @@ async function loadOrCreateSandboxRow(github, project, userId) {
1443
1474
  return github.sourceControlStorage.sandboxes.getOrCreate({ projectRepository: project, userId });
1444
1475
  }
1445
1476
  async function prepareProject(options) {
1446
- const { github, fleet, project, userId, onProgress } = options;
1447
- const sandboxRow = await loadOrCreateSandboxRow(github, project, userId);
1477
+ const { github, fleet, userId, onProgress } = options;
1478
+ let project = options.project;
1479
+ if (project.sandboxProvider !== fleet.provider) {
1480
+ const sandboxWorkdir = fleet.computeWorkdir(project.repository.slug);
1481
+ await github.sourceControlStorage.projectRepositories.update({
1482
+ orgId: project.installation.orgId,
1483
+ id: project.id,
1484
+ input: { sandboxProvider: fleet.provider, sandboxWorkdir }
1485
+ });
1486
+ project = { ...project, sandboxProvider: fleet.provider, sandboxWorkdir };
1487
+ }
1488
+ let sandboxRow = await loadOrCreateSandboxRow(github, project, userId);
1489
+ if (sandboxRow.sandboxWorkdir !== project.sandboxWorkdir) {
1490
+ await github.sourceControlStorage.sandboxes.setWorkdir({
1491
+ id: sandboxRow.id,
1492
+ sandboxWorkdir: project.sandboxWorkdir
1493
+ });
1494
+ sandboxRow = { ...sandboxRow, sandboxWorkdir: project.sandboxWorkdir, materializedAt: null };
1495
+ }
1448
1496
  const access = await github.versionControl.getRepositoryAccess({
1449
1497
  orgId: project.installation.orgId,
1450
1498
  repositoryId: project.repository.id
@@ -3015,6 +3063,17 @@ function retryDelay(error, fallbackMs) {
3015
3063
  // src/integrations/platform/github/integration.ts
3016
3064
  var PAGE_SIZE = 30;
3017
3065
  var API_PREFIX2 = "/v1/server";
3066
+ var INSTALLATION_REPOS_CACHE_TTL_MS = 3e4;
3067
+ var REPOSITORY_ACCESS_CACHE_TTL_MS = 5 * 6e4;
3068
+ var MAX_CACHE_ENTRIES = 1e3;
3069
+ function setBounded(cache, key, value) {
3070
+ cache.delete(key);
3071
+ cache.set(key, value);
3072
+ if (cache.size > MAX_CACHE_ENTRIES) {
3073
+ const oldest = cache.keys().next().value;
3074
+ if (oldest !== void 0) cache.delete(oldest);
3075
+ }
3076
+ }
3018
3077
  var REPOSITORY_TOKEN_PERMISSIONS = {
3019
3078
  contents: "write",
3020
3079
  issues: "write",
@@ -3034,6 +3093,10 @@ var PlatformGithubIntegration = class {
3034
3093
  #pollingIntervalMs;
3035
3094
  #storage;
3036
3095
  #integrationStorage;
3096
+ /** installationId → cached repository listing (TTL-bounded). */
3097
+ #installationReposCache = /* @__PURE__ */ new Map();
3098
+ /** `orgId:repositoryId` → cached repository access (TTL-bounded). */
3099
+ #repositoryAccessCache = /* @__PURE__ */ new Map();
3037
3100
  intake = {
3038
3101
  resolveIntakeDispatch: (input) => this.#resolveIntakeDispatch(input),
3039
3102
  listSources: async ({ orgId, userId }) => {
@@ -3164,6 +3227,10 @@ var PlatformGithubIntegration = class {
3164
3227
  )
3165
3228
  ),
3166
3229
  getRepositoryAccess: async ({ orgId, repositoryId }) => {
3230
+ const cacheKey = `${orgId}:${repositoryId}`;
3231
+ const cached = this.#repositoryAccessCache.get(cacheKey);
3232
+ if (cached && cached.expiresAt > Date.now()) return cached.access;
3233
+ this.#repositoryAccessCache.delete(cacheKey);
3167
3234
  const repository = await this.storage.repositories.get({ orgId, id: repositoryId });
3168
3235
  if (!repository) throw new Error("Version-control repository not found.");
3169
3236
  const cloneUrl = `https://github.com/${repository.slug}.git`;
@@ -3177,10 +3244,15 @@ var PlatformGithubIntegration = class {
3177
3244
  `${API_PREFIX2}/github-app/installations/${installationId}/token`,
3178
3245
  { repositories: [repositoryName], permissions: REPOSITORY_TOKEN_PERMISSIONS }
3179
3246
  );
3180
- return {
3247
+ const access = {
3181
3248
  cloneUrl,
3182
3249
  authorization: { scheme: "bearer", token: token.token }
3183
3250
  };
3251
+ setBounded(this.#repositoryAccessCache, cacheKey, {
3252
+ access,
3253
+ expiresAt: Date.now() + REPOSITORY_ACCESS_CACHE_TTL_MS
3254
+ });
3255
+ return access;
3184
3256
  },
3185
3257
  listPullRequests: (input) => this.#listPullRequests(input),
3186
3258
  getPullRequest: (input) => this.#getPullRequest(input),
@@ -3475,8 +3547,16 @@ var PlatformGithubIntegration = class {
3475
3547
  }
3476
3548
  }
3477
3549
  async listInstallationRepos(installationId) {
3550
+ const cached = this.#installationReposCache.get(installationId);
3551
+ if (cached && cached.expiresAt > Date.now()) return cached.repos;
3552
+ this.#installationReposCache.delete(installationId);
3478
3553
  const result = await this.#client.request("GET", `${API_PREFIX2}/github-app/installations/${installationId}/repositories`);
3479
- return result.repositories.map((repository) => ({ ...repository, installationId }));
3554
+ const repos = result.repositories.map((repository) => ({ ...repository, installationId }));
3555
+ setBounded(this.#installationReposCache, installationId, {
3556
+ repos,
3557
+ expiresAt: Date.now() + INSTALLATION_REPOS_CACHE_TTL_MS
3558
+ });
3559
+ return repos;
3480
3560
  }
3481
3561
  async mintInstallationToken(installationId) {
3482
3562
  const repositories = await this.listInstallationRepos(installationId);