@mastra/factory 0.21.1-alpha.3 → 0.22.0-alpha.4

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 (174) hide show
  1. package/dist/capabilities/version-control.d.ts +13 -8
  2. package/dist/capabilities/version-control.d.ts.map +1 -1
  3. package/dist/environment/build-runner.d.ts +72 -0
  4. package/dist/environment/build-runner.d.ts.map +1 -0
  5. package/dist/environment/build-runner.js +131 -0
  6. package/dist/environment/build-runner.js.map +1 -0
  7. package/dist/environment/build-schedule.d.ts +40 -0
  8. package/dist/environment/build-schedule.d.ts.map +1 -0
  9. package/dist/environment/build-schedule.js +94 -0
  10. package/dist/environment/build-schedule.js.map +1 -0
  11. package/dist/environment/build-workflow.d.ts +62 -0
  12. package/dist/environment/build-workflow.d.ts.map +1 -0
  13. package/dist/environment/build-workflow.js +149 -0
  14. package/dist/environment/build-workflow.js.map +1 -0
  15. package/dist/environment/build.d.ts +111 -0
  16. package/dist/environment/build.d.ts.map +1 -0
  17. package/dist/environment/build.js +344 -0
  18. package/dist/environment/build.js.map +1 -0
  19. package/dist/environment/heads.d.ts +36 -0
  20. package/dist/environment/heads.d.ts.map +1 -0
  21. package/dist/environment/heads.js +60 -0
  22. package/dist/environment/heads.js.map +1 -0
  23. package/dist/factory.d.ts +18 -4
  24. package/dist/factory.d.ts.map +1 -1
  25. package/dist/factory.js +89 -15
  26. package/dist/factory.js.map +1 -1
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -2
  30. package/dist/integrations/base.d.ts +17 -2
  31. package/dist/integrations/base.d.ts.map +1 -1
  32. package/dist/integrations/github/commits.d.ts +11 -0
  33. package/dist/integrations/github/commits.d.ts.map +1 -1
  34. package/dist/integrations/github/commits.js +20 -1
  35. package/dist/integrations/github/commits.js.map +1 -1
  36. package/dist/integrations/github/config.d.ts +3 -3
  37. package/dist/integrations/github/config.d.ts.map +1 -1
  38. package/dist/integrations/github/config.js +1 -1
  39. package/dist/integrations/github/config.js.map +1 -1
  40. package/dist/integrations/github/routes.d.ts +2 -2
  41. package/dist/integrations/github/routes.d.ts.map +1 -1
  42. package/dist/integrations/github/routes.js +6 -5
  43. package/dist/integrations/github/routes.js.map +1 -1
  44. package/dist/integrations/github/rules.d.ts +3 -1
  45. package/dist/integrations/github/rules.d.ts.map +1 -1
  46. package/dist/integrations/github/rules.js +31 -1
  47. package/dist/integrations/github/rules.js.map +1 -1
  48. package/dist/integrations/github/sandbox.d.ts +35 -0
  49. package/dist/integrations/github/sandbox.d.ts.map +1 -1
  50. package/dist/integrations/github/sandbox.js +203 -2
  51. package/dist/integrations/github/sandbox.js.map +1 -1
  52. package/dist/integrations/github/session-subscriptions.d.ts.map +1 -1
  53. package/dist/integrations/github/session-subscriptions.js +117 -62
  54. package/dist/integrations/github/session-subscriptions.js.map +1 -1
  55. package/dist/integrations/github/token-refresh.d.ts +7 -0
  56. package/dist/integrations/github/token-refresh.d.ts.map +1 -1
  57. package/dist/integrations/github/token-refresh.js.map +1 -1
  58. package/dist/integrations/github/webhook.js.map +1 -1
  59. package/dist/integrations/gitlab/agent-tools.js +1 -1
  60. package/dist/integrations/gitlab/routes.d.ts +2 -2
  61. package/dist/integrations/gitlab/routes.d.ts.map +1 -1
  62. package/dist/integrations/gitlab/routes.js.map +1 -1
  63. package/dist/integrations/gitlab/session-subscriptions.d.ts.map +1 -1
  64. package/dist/integrations/gitlab/session-subscriptions.js +8 -7
  65. package/dist/integrations/gitlab/session-subscriptions.js.map +1 -1
  66. package/dist/integrations/incidentio/agent-tools.js +1 -1
  67. package/dist/integrations/jira/agent-tools.js +1 -1
  68. package/dist/integrations/linear/agent-tools.js +1 -1
  69. package/dist/integrations/platform/github/integration.d.ts.map +1 -1
  70. package/dist/integrations/platform/github/integration.js +46 -2
  71. package/dist/integrations/platform/github/integration.js.map +1 -1
  72. package/dist/integrations/platform/jira/agent-tools.js +1 -1
  73. package/dist/integrations/slack/slack.d.ts.map +1 -1
  74. package/dist/integrations/slack/slack.js +15 -16
  75. package/dist/integrations/slack/slack.js.map +1 -1
  76. package/dist/integrations/subscription-session.js +1 -1
  77. package/dist/packages/_internals/workspace/dist/index.js +21 -1
  78. package/dist/packages/_internals/workspace/dist/index.js.map +1 -1
  79. package/dist/routes/contracts.d.ts +386 -2
  80. package/dist/routes/contracts.d.ts.map +1 -1
  81. package/dist/routes/contracts.js +166 -1
  82. package/dist/routes/contracts.js.map +1 -1
  83. package/dist/routes/fs.d.ts +25 -0
  84. package/dist/routes/fs.d.ts.map +1 -1
  85. package/dist/routes/fs.js +142 -46
  86. package/dist/routes/fs.js.map +1 -1
  87. package/dist/routes/projects.d.ts +6 -0
  88. package/dist/routes/projects.d.ts.map +1 -1
  89. package/dist/routes/projects.js +344 -1
  90. package/dist/routes/projects.js.map +1 -1
  91. package/dist/routes/source-control-sessions.d.ts +2 -0
  92. package/dist/routes/source-control-sessions.d.ts.map +1 -1
  93. package/dist/routes/source-control-sessions.js +36 -1
  94. package/dist/routes/source-control-sessions.js.map +1 -1
  95. package/dist/routes/supervisor.js +1 -1
  96. package/dist/routes/surface.d.ts +6 -3
  97. package/dist/routes/surface.d.ts.map +1 -1
  98. package/dist/routes/surface.js +21 -34
  99. package/dist/routes/surface.js.map +1 -1
  100. package/dist/rules/binding-context.d.ts +1 -1
  101. package/dist/rules/binding-context.d.ts.map +1 -1
  102. package/dist/rules/binding-context.js +1 -1
  103. package/dist/rules/binding-context.js.map +1 -1
  104. package/dist/rules/review-source-tool.js +1 -1
  105. package/dist/rules/start-coordinator.d.ts.map +1 -1
  106. package/dist/rules/start-coordinator.js +7 -2
  107. package/dist/rules/start-coordinator.js.map +1 -1
  108. package/dist/rules/tools.js +1 -1
  109. package/dist/sandbox/callback-factory-sandbox.d.ts +15 -0
  110. package/dist/sandbox/callback-factory-sandbox.d.ts.map +1 -0
  111. package/dist/sandbox/callback-factory-sandbox.js +22 -0
  112. package/dist/sandbox/callback-factory-sandbox.js.map +1 -0
  113. package/dist/sandbox/local-factory-sandbox.d.ts +20 -0
  114. package/dist/sandbox/local-factory-sandbox.d.ts.map +1 -0
  115. package/dist/sandbox/local-factory-sandbox.js +27 -0
  116. package/dist/sandbox/local-factory-sandbox.js.map +1 -0
  117. package/dist/sandbox/session-retirement.d.ts.map +1 -1
  118. package/dist/sandbox/session-retirement.js +23 -3
  119. package/dist/sandbox/session-retirement.js.map +1 -1
  120. package/dist/sandbox/session-sandbox.d.ts +82 -40
  121. package/dist/sandbox/session-sandbox.d.ts.map +1 -1
  122. package/dist/sandbox/session-sandbox.js +130 -13
  123. package/dist/sandbox/session-sandbox.js.map +1 -1
  124. package/dist/sandbox/workdir.d.ts +10 -0
  125. package/dist/sandbox/workdir.d.ts.map +1 -1
  126. package/dist/sandbox/workdir.js +10 -1
  127. package/dist/sandbox/workdir.js.map +1 -1
  128. package/dist/session/environment-repositories.d.ts +31 -0
  129. package/dist/session/environment-repositories.d.ts.map +1 -0
  130. package/dist/session/environment-repositories.js +89 -0
  131. package/dist/session/environment-repositories.js.map +1 -0
  132. package/dist/session/environment-state-processor.d.ts +59 -0
  133. package/dist/session/environment-state-processor.d.ts.map +1 -0
  134. package/dist/session/environment-state-processor.js +105 -0
  135. package/dist/session/environment-state-processor.js.map +1 -0
  136. package/dist/session/factory-session.d.ts +26 -35
  137. package/dist/session/factory-session.d.ts.map +1 -1
  138. package/dist/session/factory-session.js +34 -71
  139. package/dist/session/factory-session.js.map +1 -1
  140. package/dist/session/filesystem-capture.d.ts +13 -0
  141. package/dist/session/filesystem-capture.d.ts.map +1 -1
  142. package/dist/session/filesystem-capture.js +73 -14
  143. package/dist/session/filesystem-capture.js.map +1 -1
  144. package/dist/session/source-control-tools.d.ts +1 -1
  145. package/dist/session/source-control-tools.d.ts.map +1 -1
  146. package/dist/session/source-control-tools.js +144 -71
  147. package/dist/session/source-control-tools.js.map +1 -1
  148. package/dist/spa-static.js +2 -2
  149. package/dist/storage/domains/audit/agent-audit.d.ts +6 -1
  150. package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -1
  151. package/dist/storage/domains/audit/agent-audit.js +31 -13
  152. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  153. package/dist/storage/domains/projects/base.d.ts +115 -2
  154. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  155. package/dist/storage/domains/projects/base.js +219 -12
  156. package/dist/storage/domains/projects/base.js.map +1 -1
  157. package/dist/storage/domains/source-control/base.d.ts +123 -7
  158. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  159. package/dist/storage/domains/source-control/base.js +406 -33
  160. package/dist/storage/domains/source-control/base.js.map +1 -1
  161. package/dist/storage/domains/source-control/inmemory.d.ts +19 -6
  162. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  163. package/dist/storage/domains/source-control/inmemory.js +90 -2
  164. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  165. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  166. package/dist/storage/domains/work-items/base.js +12 -5
  167. package/dist/storage/domains/work-items/base.js.map +1 -1
  168. package/dist/supervisor/read-tools.js +1 -1
  169. package/dist/supervisor/write-tools.js +1 -1
  170. package/dist/workspace.d.ts +50 -7
  171. package/dist/workspace.d.ts.map +1 -1
  172. package/dist/workspace.js +344 -24
  173. package/dist/workspace.js.map +1 -1
  174. package/package.json +7 -7
@@ -1,3 +1,4 @@
1
+ import { FACTORY_PROJECTS_SCHEMA } from "../projects/base.js";
1
2
  import { FactoryStorageDomain, UniqueViolationError } from "@mastra/core/storage";
2
3
  //#region src/storage/domains/source-control/base.ts
3
4
  const FACTORY_PROJECTS = "factory_projects";
@@ -6,6 +7,20 @@ const REPOSITORIES = "source_control_repositories";
6
7
  const CONNECTIONS = "factory_project_source_control_connections";
7
8
  const PROJECT_REPOSITORIES = "factory_project_repositories";
8
9
  const SESSIONS = "source_control_sessions";
10
+ const SESSION_REPOSITORIES = "source_control_session_repositories";
11
+ /**
12
+ * The user already holds the branch on this link in a session without this
13
+ * factory's ownership, left by the factory migration. It is never reused and
14
+ * cannot be replaced while it holds the branch.
15
+ */
16
+ var SessionOwnershipMismatchError = class extends Error {
17
+ sessionId;
18
+ constructor(sessionId) {
19
+ super(`Session ${sessionId} already holds this branch but does not belong to this factory.`);
20
+ this.sessionId = sessionId;
21
+ this.name = "SessionOwnershipMismatchError";
22
+ }
23
+ };
9
24
  var SourceControlConnectionNotFoundError = class extends Error {
10
25
  constructor() {
11
26
  super("Project source-control connection not found for this organization and integration.");
@@ -109,6 +124,26 @@ const SOURCE_CONTROL_SCHEMAS = [
109
124
  type: "text",
110
125
  nullable: true
111
126
  },
127
+ position: {
128
+ type: "integer",
129
+ default: 0
130
+ },
131
+ in_environment: {
132
+ type: "boolean",
133
+ default: true
134
+ },
135
+ last_build_status: {
136
+ type: "text",
137
+ default: "unbuilt"
138
+ },
139
+ last_build_error: {
140
+ type: "text",
141
+ nullable: true
142
+ },
143
+ last_built_at: {
144
+ type: "timestamp",
145
+ nullable: true
146
+ },
112
147
  created_at: { type: "timestamp" },
113
148
  updated_at: { type: "timestamp" }
114
149
  },
@@ -127,6 +162,10 @@ const SOURCE_CONTROL_SCHEMAS = [
127
162
  id: { type: "uuid-pk" },
128
163
  session_id: { type: "text" },
129
164
  project_repository_id: { type: "text" },
165
+ factory_project_id: {
166
+ type: "text",
167
+ nullable: true
168
+ },
130
169
  org_id: { type: "text" },
131
170
  user_id: { type: "text" },
132
171
  branch: { type: "text" },
@@ -162,16 +201,59 @@ const SOURCE_CONTROL_SCHEMAS = [
162
201
  created_at: { type: "timestamp" },
163
202
  updated_at: { type: "timestamp" }
164
203
  },
204
+ uniqueIndexes: [
205
+ {
206
+ name: "source_control_sessions_session_id_unique",
207
+ columns: ["session_id"]
208
+ },
209
+ {
210
+ name: "source_control_sessions_repository_user_branch_unique",
211
+ columns: [
212
+ "project_repository_id",
213
+ "user_id",
214
+ "branch"
215
+ ]
216
+ },
217
+ {
218
+ name: "source_control_sessions_project_user_branch_unique",
219
+ columns: [
220
+ "factory_project_id",
221
+ "user_id",
222
+ "branch"
223
+ ]
224
+ }
225
+ ],
226
+ indexes: [{
227
+ name: "source_control_sessions_project_idx",
228
+ columns: ["factory_project_id"]
229
+ }]
230
+ },
231
+ {
232
+ name: SESSION_REPOSITORIES,
233
+ columns: {
234
+ id: { type: "uuid-pk" },
235
+ session_id: { type: "text" },
236
+ project_repository_id: { type: "text" },
237
+ branch: { type: "text" },
238
+ change_request_id: {
239
+ type: "text",
240
+ nullable: true
241
+ },
242
+ change_request_url: {
243
+ type: "text",
244
+ nullable: true
245
+ },
246
+ pushed_at: { type: "timestamp" },
247
+ created_at: { type: "timestamp" },
248
+ updated_at: { type: "timestamp" }
249
+ },
165
250
  uniqueIndexes: [{
166
- name: "source_control_sessions_session_id_unique",
251
+ name: "source_control_session_repositories_session_link_unique",
252
+ columns: ["session_id", "project_repository_id"]
253
+ }],
254
+ indexes: [{
255
+ name: "source_control_session_repositories_session_idx",
167
256
  columns: ["session_id"]
168
- }, {
169
- name: "source_control_sessions_repository_user_branch_unique",
170
- columns: [
171
- "project_repository_id",
172
- "user_id",
173
- "branch"
174
- ]
175
257
  }]
176
258
  }
177
259
  ];
@@ -221,6 +303,31 @@ function toProjectRepository(row) {
221
303
  sandboxWorkdir: row.sandbox_workdir,
222
304
  setupCommand: row.setup_command,
223
305
  teardownCommand: row.teardown_command,
306
+ position: row.position ?? 0,
307
+ inEnvironment: row.in_environment ?? true,
308
+ lastBuildStatus: toBuildStatus(row.last_build_status),
309
+ lastBuildError: row.last_build_error,
310
+ lastBuiltAt: row.last_built_at,
311
+ createdAt: row.created_at,
312
+ updatedAt: row.updated_at
313
+ };
314
+ }
315
+ function toBuildStatus(value) {
316
+ return value === "configured" || value === "failed" ? value : "unbuilt";
317
+ }
318
+ /** Oldest first; ties (same millisecond) break on id so re-runs see one order. */
319
+ function byCreatedAt(a, b) {
320
+ return a.created_at.getTime() - b.created_at.getTime() || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
321
+ }
322
+ function toSessionRepository(row) {
323
+ return {
324
+ id: row.id,
325
+ sessionId: row.session_id,
326
+ projectRepositoryId: row.project_repository_id,
327
+ branch: row.branch,
328
+ changeRequestId: row.change_request_id,
329
+ changeRequestUrl: row.change_request_url,
330
+ pushedAt: row.pushed_at,
224
331
  createdAt: row.created_at,
225
332
  updatedAt: row.updated_at
226
333
  };
@@ -230,6 +337,7 @@ function toSession(row) {
230
337
  id: row.id,
231
338
  sessionId: row.session_id,
232
339
  projectRepositoryId: row.project_repository_id,
340
+ factoryProjectId: row.factory_project_id ?? null,
233
341
  orgId: row.org_id,
234
342
  userId: row.user_id,
235
343
  branch: row.branch,
@@ -245,14 +353,162 @@ function toSession(row) {
245
353
  updatedAt: row.updated_at
246
354
  };
247
355
  }
356
+ function projectRepositoryPatch(input) {
357
+ const patch = { updated_at: /* @__PURE__ */ new Date() };
358
+ if (input.branch !== void 0) patch.branch = input.branch;
359
+ if (input.sandboxProvider !== void 0) patch.sandbox_provider = input.sandboxProvider;
360
+ if (input.sandboxWorkdir !== void 0) patch.sandbox_workdir = input.sandboxWorkdir;
361
+ if (input.setupCommand !== void 0) patch.setup_command = input.setupCommand;
362
+ if (input.teardownCommand !== void 0) patch.teardown_command = input.teardownCommand;
363
+ if (input.position !== void 0) patch.position = input.position;
364
+ if (input.inEnvironment !== void 0) patch.in_environment = input.inEnvironment;
365
+ return patch;
366
+ }
248
367
  var SourceControlStorage = class extends FactoryStorageDomain {
249
368
  constructor() {
250
369
  super("source-control");
251
370
  }
252
371
  async init() {
253
372
  await this.ensureCollections(SOURCE_CONTROL_SCHEMAS);
373
+ await this.#backfillEnvironment();
374
+ await this.#backfillSessionFactory();
375
+ }
376
+ /**
377
+ * Fill `factory_project_id` on sessions that predate the column, walking
378
+ * link → connection. The old constraint was per link, so two links of one
379
+ * factory may hold the same user and branch; the new per-factory index
380
+ * would reject the second write and a throwing `init()` only leaves the
381
+ * domain not-ready. Within such a group the oldest session wins and the
382
+ * others keep a null factory. Idempotent, O(rows), deletes nothing.
383
+ */
384
+ async #backfillSessionFactory() {
385
+ let unresolved = 0;
386
+ let collisions = 0;
387
+ try {
388
+ const pending = await this.ops.findMany(SESSIONS, { factory_project_id: null });
389
+ if (pending.length === 0) return;
390
+ const connections = await this.ops.findMany(CONNECTIONS, {});
391
+ const links = await this.ops.findMany(PROJECT_REPOSITORIES, {});
392
+ const projectByConnectionId = new Map(connections.map((row) => [row.id, row.factory_project_id]));
393
+ const projectByLinkId = new Map(links.flatMap((link) => {
394
+ const projectId = projectByConnectionId.get(link.connection_id);
395
+ return projectId ? [[link.id, projectId]] : [];
396
+ }));
397
+ const keyOf = (projectId, row) => `${projectId}\u0000${row.user_id}\u0000${row.branch}`;
398
+ const taken = /* @__PURE__ */ new Set();
399
+ for (const row of await this.ops.findMany(SESSIONS, {})) if (row.factory_project_id) taken.add(keyOf(row.factory_project_id, row));
400
+ const ordered = [...pending].sort((a, b) => a.created_at.getTime() - b.created_at.getTime() || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
401
+ for (const row of ordered) {
402
+ const projectId = projectByLinkId.get(row.project_repository_id);
403
+ if (!projectId) {
404
+ unresolved += 1;
405
+ continue;
406
+ }
407
+ const key = keyOf(projectId, row);
408
+ if (taken.has(key)) {
409
+ collisions += 1;
410
+ continue;
411
+ }
412
+ try {
413
+ await this.ops.updateMany(SESSIONS, { id: row.id }, { factory_project_id: projectId });
414
+ taken.add(key);
415
+ } catch (error) {
416
+ if (!(error instanceof UniqueViolationError)) throw error;
417
+ collisions += 1;
418
+ }
419
+ }
420
+ } catch (error) {
421
+ console.warn(`[factory] session backfill stopped early: ${error instanceof Error ? error.message : error}`);
422
+ return;
423
+ }
424
+ if (unresolved > 0 || collisions > 0) console.warn(`[factory] session backfill: ${unresolved} sessions have no resolvable factory, ${collisions} sessions collide on (factory, user, branch) and keep a null factory`);
425
+ }
426
+ /**
427
+ * One-time fill of the environment columns from the rows that predate them.
428
+ * Idempotent and non-destructive: a project is touched only while its
429
+ * `environment_backfilled_at` is null, every other write lands only where the
430
+ * stored value is still the column default, and nothing is deleted. Runs
431
+ * on every start; reports what it could not resolve instead of throwing,
432
+ * because a throwing `init()` only leaves the domain not-ready.
433
+ */
434
+ async #backfillEnvironment() {
435
+ await this.ensureCollections([FACTORY_PROJECTS_SCHEMA]);
436
+ let missingRepositories = 0;
437
+ const failedProjects = [];
438
+ try {
439
+ const projects = await this.ops.findMany(FACTORY_PROJECTS, { environment_backfilled_at: null });
440
+ if (projects.length === 0) return;
441
+ const connections = await this.ops.findMany(CONNECTIONS, {});
442
+ const links = await this.ops.findMany(PROJECT_REPOSITORIES, {});
443
+ const repositories = await this.ops.findMany(REPOSITORIES, {});
444
+ const slugByRepositoryId = new Map(repositories.map((row) => [row.id, row.slug]));
445
+ const linksByConnectionId = /* @__PURE__ */ new Map();
446
+ for (const link of links) {
447
+ const bucket = linksByConnectionId.get(link.connection_id) ?? [];
448
+ bucket.push(link);
449
+ linksByConnectionId.set(link.connection_id, bucket);
450
+ }
451
+ const connectionsByProjectId = /* @__PURE__ */ new Map();
452
+ for (const connection of connections) {
453
+ const bucket = connectionsByProjectId.get(connection.factory_project_id) ?? [];
454
+ bucket.push(connection);
455
+ connectionsByProjectId.set(connection.factory_project_id, bucket);
456
+ }
457
+ for (const project of projects) try {
458
+ const projectLinks = (connectionsByProjectId.get(project.id) ?? []).flatMap((connection) => linksByConnectionId.get(connection.id) ?? []).sort(byCreatedAt);
459
+ const slugOf = (link) => {
460
+ const slug = slugByRepositoryId.get(link.repository_id);
461
+ if (slug !== void 0) return slug;
462
+ missingRepositories += 1;
463
+ return `missing-repository:${link.id}`;
464
+ };
465
+ let nextPosition = Math.max(0, ...projectLinks.map((link) => link.position ?? 0));
466
+ const seenSlugs = new Set(projectLinks.filter((link) => (link.position ?? 0) > 0 && link.in_environment !== false).map(slugOf));
467
+ for (const link of projectLinks) {
468
+ if ((link.position ?? 0) > 0) continue;
469
+ nextPosition += 1;
470
+ const slug = slugOf(link);
471
+ const inEnvironment = link.in_environment !== false && !seenSlugs.has(slug);
472
+ if (inEnvironment) seenSlugs.add(slug);
473
+ await this.ops.updateMany(PROJECT_REPOSITORIES, { id: link.id }, {
474
+ position: nextPosition,
475
+ in_environment: inEnvironment
476
+ });
477
+ }
478
+ await this.ops.updateMany(FACTORY_PROJECTS, { id: project.id }, { environment_backfilled_at: /* @__PURE__ */ new Date() });
479
+ } catch {
480
+ failedProjects.push(project.id);
481
+ }
482
+ } catch (error) {
483
+ console.warn(`[factory] environment backfill did not run: ${error instanceof Error ? error.message : error}`);
484
+ return;
485
+ }
486
+ if (missingRepositories > 0 || failedProjects.length > 0) console.warn(`[factory] environment backfill: ${missingRepositories} links point at a missing repository row, ${failedProjects.length} projects could not be updated${failedProjects.length > 0 ? ` (${failedProjects.join(", ")})` : ""}`);
487
+ }
488
+ /**
489
+ * Applies environment patches to a project's repository links through `ops`,
490
+ * normally a transaction that also holds the project row. Validates against
491
+ * the links as they are inside that transaction: every id must belong to the
492
+ * project, and a reorder must list every link so no two share a position.
493
+ * Nothing is written when validation fails.
494
+ */
495
+ async updateEnvironmentRepositories(ops, args) {
496
+ const connections = args.integrationIds.length === 0 ? [] : await ops.findMany(CONNECTIONS, {
497
+ factory_project_id: args.factoryProjectId,
498
+ integration_id: { in: args.integrationIds }
499
+ });
500
+ const links = connections.length === 0 ? [] : await ops.findMany(PROJECT_REPOSITORIES, { connection_id: { in: connections.map((connection) => connection.id) } });
501
+ const linkIds = new Set(links.map((link) => link.id));
502
+ if (args.patches.some((patch) => !linkIds.has(patch.projectRepositoryId))) return "not_found";
503
+ if (args.patches.some((patch) => patch.position !== void 0) && args.patches.length !== linkIds.size) return "incomplete_reorder";
504
+ for (const { projectRepositoryId, ...input } of args.patches) {
505
+ if (Object.keys(input).length === 0) continue;
506
+ await ops.updateMany(PROJECT_REPOSITORIES, { id: projectRepositoryId }, projectRepositoryPatch(input));
507
+ }
508
+ return "updated";
254
509
  }
255
510
  async dangerouslyClearAll() {
511
+ await this.ops.deleteMany(SESSION_REPOSITORIES, {});
256
512
  await this.ops.deleteMany(SESSIONS, {});
257
513
  await this.ops.deleteMany(PROJECT_REPOSITORIES, {});
258
514
  await this.ops.deleteMany(CONNECTIONS, {});
@@ -497,7 +753,11 @@ var SourceControlStorage = class extends FactoryStorageDomain {
497
753
  id
498
754
  })) return false;
499
755
  const projectRepositories = await db().findMany(PROJECT_REPOSITORIES, { connection_id: id });
500
- for (const projectRepository of projectRepositories) await db().deleteMany(SESSIONS, { project_repository_id: projectRepository.id });
756
+ for (const projectRepository of projectRepositories) {
757
+ for (const session of await db().findMany(SESSIONS, { project_repository_id: projectRepository.id })) await db().deleteMany(SESSION_REPOSITORIES, { session_id: session.session_id });
758
+ await db().deleteMany(SESSION_REPOSITORIES, { project_repository_id: projectRepository.id });
759
+ await db().deleteMany(SESSIONS, { project_repository_id: projectRepository.id });
760
+ }
501
761
  await db().deleteMany(PROJECT_REPOSITORIES, { connection_id: id });
502
762
  await db().deleteMany(CONNECTIONS, {
503
763
  id,
@@ -568,16 +828,40 @@ var SourceControlStorage = class extends FactoryStorageDomain {
568
828
  }
569
829
  return targets;
570
830
  },
831
+ listByProject: async ({ orgId, factoryProjectId }) => {
832
+ if (!await db().findOne(FACTORY_PROJECTS, {
833
+ id: factoryProjectId,
834
+ org_id: orgId
835
+ })) return [];
836
+ const connections = await db().findMany(CONNECTIONS, {
837
+ factory_project_id: factoryProjectId,
838
+ integration_id: integrationId
839
+ });
840
+ if (connections.length === 0) return [];
841
+ return (await db().findMany(PROJECT_REPOSITORIES, { connection_id: { in: connections.map((connection) => connection.id) } }, { orderBy: [["position", "asc"], ["created_at", "asc"]] })).map(toProjectRepository);
842
+ },
571
843
  get: getProjectRepository,
572
844
  link: async (input) => {
573
845
  const connection = await requireConnection({
574
846
  orgId: input.orgId,
575
847
  id: input.connectionId
576
848
  });
577
- if ((await requireRepository({
849
+ const repository = await requireRepository({
578
850
  orgId: input.orgId,
579
851
  id: input.repositoryId
580
- })).installationId !== connection.installationId) throw new Error("Repository does not belong to the connection installation.");
852
+ });
853
+ if (repository.installationId !== connection.installationId) throw new Error("Repository does not belong to the connection installation.");
854
+ const siblingConnections = await db().findMany(CONNECTIONS, { factory_project_id: connection.factoryProjectId });
855
+ const siblings = await db().findMany(PROJECT_REPOSITORIES, { connection_id: { in: siblingConnections.map((sibling) => sibling.id) } });
856
+ const position = 1 + Math.max(0, ...siblings.map((sibling) => sibling.position ?? 0));
857
+ let inEnvironment = true;
858
+ for (const sibling of siblings) {
859
+ if (sibling.in_environment === false) continue;
860
+ if ((await db().findOne(REPOSITORIES, { id: sibling.repository_id }))?.slug === repository.slug) {
861
+ inEnvironment = false;
862
+ break;
863
+ }
864
+ }
581
865
  const now = /* @__PURE__ */ new Date();
582
866
  try {
583
867
  return toProjectRepository(await db().insertOne(PROJECT_REPOSITORIES, {
@@ -589,6 +873,11 @@ var SourceControlStorage = class extends FactoryStorageDomain {
589
873
  sandbox_workdir: input.sandboxWorkdir,
590
874
  setup_command: input.setupCommand ?? null,
591
875
  teardown_command: input.teardownCommand ?? null,
876
+ position,
877
+ in_environment: inEnvironment,
878
+ last_build_status: "unbuilt",
879
+ last_build_error: null,
880
+ last_built_at: null,
592
881
  created_at: now,
593
882
  updated_at: now
594
883
  }));
@@ -607,13 +896,23 @@ var SourceControlStorage = class extends FactoryStorageDomain {
607
896
  orgId,
608
897
  id
609
898
  })) return null;
610
- const patch = { updated_at: /* @__PURE__ */ new Date() };
611
- if (input.branch !== void 0) patch.branch = input.branch;
612
- if (input.sandboxProvider !== void 0) patch.sandbox_provider = input.sandboxProvider;
613
- if (input.sandboxWorkdir !== void 0) patch.sandbox_workdir = input.sandboxWorkdir;
614
- if (input.setupCommand !== void 0) patch.setup_command = input.setupCommand;
615
- if (input.teardownCommand !== void 0) patch.teardown_command = input.teardownCommand;
616
- await db().updateMany(PROJECT_REPOSITORIES, { id }, patch);
899
+ await db().updateMany(PROJECT_REPOSITORIES, { id }, projectRepositoryPatch(input));
900
+ return getProjectRepository({
901
+ orgId,
902
+ id
903
+ });
904
+ },
905
+ setBuildStatus: async ({ orgId, id, status, error, builtAt }) => {
906
+ if (!await getProjectRepository({
907
+ orgId,
908
+ id
909
+ })) return null;
910
+ await db().updateMany(PROJECT_REPOSITORIES, { id }, {
911
+ last_build_status: status,
912
+ last_build_error: error ?? null,
913
+ last_built_at: builtAt ?? null,
914
+ updated_at: /* @__PURE__ */ new Date()
915
+ });
617
916
  return getProjectRepository({
618
917
  orgId,
619
918
  id
@@ -624,6 +923,8 @@ var SourceControlStorage = class extends FactoryStorageDomain {
624
923
  orgId,
625
924
  id
626
925
  })) return false;
926
+ for (const session of await db().findMany(SESSIONS, { project_repository_id: id })) await db().deleteMany(SESSION_REPOSITORIES, { session_id: session.session_id });
927
+ await db().deleteMany(SESSION_REPOSITORIES, { project_repository_id: id });
627
928
  await db().deleteMany(SESSIONS, { project_repository_id: id });
628
929
  await db().deleteMany(PROJECT_REPOSITORIES, { id });
629
930
  return true;
@@ -638,6 +939,19 @@ var SourceControlStorage = class extends FactoryStorageDomain {
638
939
  if (!await getProjectRepositoryById(projectRepositoryId)) return [];
639
940
  return (await db().findMany(SESSIONS, { project_repository_id: projectRepositoryId })).map(toSession);
640
941
  },
942
+ listByProject: async ({ orgId, factoryProjectId, viewerUserId }) => {
943
+ const connections = await db().findMany(CONNECTIONS, {
944
+ factory_project_id: factoryProjectId,
945
+ integration_id: integrationId
946
+ });
947
+ if (connections.length === 0) return [];
948
+ const linkIds = /* @__PURE__ */ new Set();
949
+ for (const connection of connections) for (const link of await db().findMany(PROJECT_REPOSITORIES, { connection_id: connection.id })) linkIds.add(link.id);
950
+ return (await db().findMany(SESSIONS, {
951
+ factory_project_id: factoryProjectId,
952
+ org_id: orgId
953
+ })).filter((row) => linkIds.has(row.project_repository_id)).filter((row) => row.visibility !== "private" || row.user_id === viewerUserId).map(toSession);
954
+ },
641
955
  getBySessionId: async (sessionId) => {
642
956
  const row = await db().findOne(SESSIONS, { session_id: sessionId });
643
957
  return row && await getProjectRepositoryById(row.project_repository_id) ? toSession(row) : null;
@@ -648,28 +962,49 @@ var SourceControlStorage = class extends FactoryStorageDomain {
648
962
  updated_at: /* @__PURE__ */ new Date()
649
963
  }));
650
964
  },
651
- getForBranch: async ({ projectRepositoryId, userId, branch }) => {
652
- if (!await getProjectRepositoryById(projectRepositoryId)) return null;
965
+ getForBranch: async (args) => {
966
+ if ("factoryProjectId" in args) {
967
+ const row = await db().findOne(SESSIONS, {
968
+ factory_project_id: args.factoryProjectId,
969
+ user_id: args.userId,
970
+ branch: args.branch
971
+ });
972
+ return row && await getProjectRepositoryById(row.project_repository_id) ? toSession(row) : null;
973
+ }
974
+ if (!await getProjectRepositoryById(args.projectRepositoryId)) return null;
653
975
  const row = await db().findOne(SESSIONS, {
654
- project_repository_id: projectRepositoryId,
655
- user_id: userId,
656
- branch
976
+ project_repository_id: args.projectRepositoryId,
977
+ user_id: args.userId,
978
+ branch: args.branch
657
979
  });
658
980
  return row ? toSession(row) : null;
659
981
  },
660
982
  create: async (input) => {
661
- await requireProjectRepositoryById(input.projectRepositoryId);
662
- const existing = await db().findOne(SESSIONS, {
983
+ const link = await requireProjectRepositoryById(input.projectRepositoryId);
984
+ const connection = await getConnection({
985
+ orgId: input.orgId,
986
+ id: link.connectionId
987
+ });
988
+ if (!connection) throw new Error("Project repository not found for this integration.");
989
+ if (input.factoryProjectId !== void 0 && input.factoryProjectId !== connection.factoryProjectId) throw new Error("Session factory does not match the repository link.");
990
+ const factoryProjectId = connection.factoryProjectId;
991
+ const reuse = (row) => {
992
+ if (row.factory_project_id !== factoryProjectId) throw new SessionOwnershipMismatchError(row.session_id);
993
+ return toSession(row);
994
+ };
995
+ const findExisting = async () => db().findOne(SESSIONS, {
663
996
  project_repository_id: input.projectRepositoryId,
664
997
  user_id: input.userId,
665
998
  branch: input.branch
666
999
  });
667
- if (existing) return toSession(existing);
1000
+ const existing = await findExisting();
1001
+ if (existing) return reuse(existing);
668
1002
  const now = /* @__PURE__ */ new Date();
669
1003
  try {
670
1004
  return toSession(await db().insertOne(SESSIONS, {
671
1005
  session_id: input.sessionId,
672
1006
  project_repository_id: input.projectRepositoryId,
1007
+ factory_project_id: factoryProjectId,
673
1008
  org_id: input.orgId,
674
1009
  user_id: input.userId,
675
1010
  branch: input.branch,
@@ -685,13 +1020,9 @@ var SourceControlStorage = class extends FactoryStorageDomain {
685
1020
  }));
686
1021
  } catch (error) {
687
1022
  if (!(error instanceof UniqueViolationError)) throw error;
688
- const row = await db().findOne(SESSIONS, {
689
- project_repository_id: input.projectRepositoryId,
690
- user_id: input.userId,
691
- branch: input.branch
692
- });
1023
+ const row = await findExisting();
693
1024
  if (!row) throw error;
694
- return toSession(row);
1025
+ return reuse(row);
695
1026
  }
696
1027
  },
697
1028
  setSandbox: async ({ id, sandboxId, sandboxWorkdir }) => {
@@ -729,13 +1060,55 @@ var SourceControlStorage = class extends FactoryStorageDomain {
729
1060
  });
730
1061
  },
731
1062
  delete: async (id) => {
1063
+ const row = await db().findOne(SESSIONS, { id });
1064
+ if (row) await db().deleteMany(SESSION_REPOSITORIES, { session_id: row.session_id });
732
1065
  await db().deleteMany(SESSIONS, { id });
733
1066
  }
1067
+ },
1068
+ sessionRepositories: {
1069
+ upsert: async (input) => {
1070
+ const key = {
1071
+ session_id: input.sessionId,
1072
+ project_repository_id: input.projectRepositoryId
1073
+ };
1074
+ const now = /* @__PURE__ */ new Date();
1075
+ const changes = { updated_at: now };
1076
+ if (input.branch !== void 0) {
1077
+ changes.branch = input.branch;
1078
+ changes.pushed_at = input.pushedAt ?? now;
1079
+ } else if (input.pushedAt) changes.pushed_at = input.pushedAt;
1080
+ if (input.changeRequestId !== void 0) changes.change_request_id = input.changeRequestId;
1081
+ if (input.changeRequestUrl !== void 0) changes.change_request_url = input.changeRequestUrl;
1082
+ const update = async () => {
1083
+ await db().updateMany(SESSION_REPOSITORIES, key, changes);
1084
+ const row = await db().findOne(SESSION_REPOSITORIES, key);
1085
+ if (!row) throw new Error("Session repository row vanished during upsert.");
1086
+ return toSessionRepository(row);
1087
+ };
1088
+ if (await db().findOne(SESSION_REPOSITORIES, key)) return update();
1089
+ const branch = input.branch ?? input.fallbackBranch;
1090
+ if (!branch) throw new Error("A session repository row needs a branch when it is first recorded.");
1091
+ try {
1092
+ return toSessionRepository(await db().insertOne(SESSION_REPOSITORIES, {
1093
+ ...key,
1094
+ branch,
1095
+ change_request_id: input.changeRequestId ?? null,
1096
+ change_request_url: input.changeRequestUrl ?? null,
1097
+ pushed_at: input.pushedAt ?? now,
1098
+ updated_at: now,
1099
+ created_at: now
1100
+ }));
1101
+ } catch (error) {
1102
+ if (!(error instanceof UniqueViolationError)) throw error;
1103
+ return update();
1104
+ }
1105
+ },
1106
+ listBySession: async ({ sessionId }) => (await db().findMany(SESSION_REPOSITORIES, { session_id: sessionId }, { orderBy: [["created_at", "asc"]] })).map(toSessionRepository)
734
1107
  }
735
1108
  };
736
1109
  }
737
1110
  };
738
1111
  //#endregion
739
- export { SOURCE_CONTROL_SCHEMAS, SourceControlConnectionNotFoundError, SourceControlStorage };
1112
+ export { SOURCE_CONTROL_SCHEMAS, SessionOwnershipMismatchError, SourceControlConnectionNotFoundError, SourceControlStorage };
740
1113
 
741
1114
  //# sourceMappingURL=base.js.map