@mastra/factory 0.21.1-alpha.2 → 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 (180) 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/secret-encryption.d.ts.map +1 -1
  129. package/dist/secret-encryption.js +12 -1
  130. package/dist/secret-encryption.js.map +1 -1
  131. package/dist/session/environment-repositories.d.ts +31 -0
  132. package/dist/session/environment-repositories.d.ts.map +1 -0
  133. package/dist/session/environment-repositories.js +89 -0
  134. package/dist/session/environment-repositories.js.map +1 -0
  135. package/dist/session/environment-state-processor.d.ts +59 -0
  136. package/dist/session/environment-state-processor.d.ts.map +1 -0
  137. package/dist/session/environment-state-processor.js +105 -0
  138. package/dist/session/environment-state-processor.js.map +1 -0
  139. package/dist/session/factory-session.d.ts +26 -35
  140. package/dist/session/factory-session.d.ts.map +1 -1
  141. package/dist/session/factory-session.js +34 -71
  142. package/dist/session/factory-session.js.map +1 -1
  143. package/dist/session/filesystem-capture.d.ts +13 -0
  144. package/dist/session/filesystem-capture.d.ts.map +1 -1
  145. package/dist/session/filesystem-capture.js +73 -14
  146. package/dist/session/filesystem-capture.js.map +1 -1
  147. package/dist/session/source-control-tools.d.ts +1 -1
  148. package/dist/session/source-control-tools.d.ts.map +1 -1
  149. package/dist/session/source-control-tools.js +144 -71
  150. package/dist/session/source-control-tools.js.map +1 -1
  151. package/dist/spa-static.js +2 -2
  152. package/dist/storage/domains/audit/agent-audit.d.ts +6 -1
  153. package/dist/storage/domains/audit/agent-audit.d.ts.map +1 -1
  154. package/dist/storage/domains/audit/agent-audit.js +31 -13
  155. package/dist/storage/domains/audit/agent-audit.js.map +1 -1
  156. package/dist/storage/domains/custom-providers/base.d.ts.map +1 -1
  157. package/dist/storage/domains/custom-providers/base.js +13 -2
  158. package/dist/storage/domains/custom-providers/base.js.map +1 -1
  159. package/dist/storage/domains/projects/base.d.ts +115 -2
  160. package/dist/storage/domains/projects/base.d.ts.map +1 -1
  161. package/dist/storage/domains/projects/base.js +219 -12
  162. package/dist/storage/domains/projects/base.js.map +1 -1
  163. package/dist/storage/domains/source-control/base.d.ts +123 -7
  164. package/dist/storage/domains/source-control/base.d.ts.map +1 -1
  165. package/dist/storage/domains/source-control/base.js +406 -33
  166. package/dist/storage/domains/source-control/base.js.map +1 -1
  167. package/dist/storage/domains/source-control/inmemory.d.ts +19 -6
  168. package/dist/storage/domains/source-control/inmemory.d.ts.map +1 -1
  169. package/dist/storage/domains/source-control/inmemory.js +90 -2
  170. package/dist/storage/domains/source-control/inmemory.js.map +1 -1
  171. package/dist/storage/domains/work-items/base.d.ts.map +1 -1
  172. package/dist/storage/domains/work-items/base.js +12 -5
  173. package/dist/storage/domains/work-items/base.js.map +1 -1
  174. package/dist/supervisor/read-tools.js +1 -1
  175. package/dist/supervisor/write-tools.js +1 -1
  176. package/dist/workspace.d.ts +50 -7
  177. package/dist/workspace.d.ts.map +1 -1
  178. package/dist/workspace.js +344 -24
  179. package/dist/workspace.js.map +1 -1
  180. package/package.json +7 -7
@@ -1,7 +1,11 @@
1
+ import { redactCredentials } from "../environment/build.js";
2
+ import { ensureSchedule, invalidCronMessage, readSchedule, scheduleIdFor } from "../environment/build-schedule.js";
1
3
  import { ACTIVE_RUN_BINDING_STAGES } from "../storage/domains/work-items/base.js";
2
4
  import { Route } from "./route.js";
3
5
  import { FACTORY_ROUTE_CONTRACTS } from "./contracts.js";
4
6
  import { registerApiRoute } from "@mastra/core/server";
7
+ import { describeFactorySandbox, normalizeFactorySandboxSettings } from "@mastra/core/workspace";
8
+ import { MastraError } from "@mastra/core/error";
5
9
  //#region src/routes/projects.ts
6
10
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
7
11
  const MAX_NAME_LENGTH = 200;
@@ -107,12 +111,66 @@ function parseRepositoryUpdateInput(value) {
107
111
  if (teardownCommand !== void 0) patch.teardownCommand = teardownCommand;
108
112
  return Object.keys(patch).length > 0 ? patch : null;
109
113
  }
114
+ /** Provider output is shown to the user; strip any clone credential from it first. */
115
+ function redactBuild(build) {
116
+ return {
117
+ ...build,
118
+ ...build.error !== void 0 ? { error: redactCredentials(build.error) } : {},
119
+ ...build.logs ? { logs: build.logs.map(redactCredentials) } : {}
120
+ };
121
+ }
122
+ /** What the environment reports when the factory has no sandbox configured. */
123
+ const NO_SANDBOX = {
124
+ provider: "none",
125
+ settingsSchema: {
126
+ type: "object",
127
+ properties: {},
128
+ additionalProperties: false
129
+ },
130
+ capabilities: {
131
+ template: false,
132
+ builds: {
133
+ available: false,
134
+ history: false
135
+ }
136
+ }
137
+ };
110
138
  var ProjectRoutes = class extends Route {
111
139
  #versionControlIntegrationIds;
140
+ #sandboxDescription;
141
+ #settingsSchema;
112
142
  constructor(deps) {
113
143
  super(deps);
114
144
  this.#versionControlIntegrationIds = new Set(deps.versionControlIntegrationIds ?? []);
115
145
  }
146
+ #describeSandbox() {
147
+ if (!this.deps.sandbox) return NO_SANDBOX;
148
+ this.#sandboxDescription ??= describeFactorySandbox(this.deps.sandbox);
149
+ return this.#sandboxDescription;
150
+ }
151
+ /**
152
+ * Merge a settings patch onto the stored document (null removes a key) and
153
+ * validate the result through the sandbox's schema. Returns the issues when
154
+ * the merged document is rejected.
155
+ *
156
+ * A stored key the current schema does not declare is left over from another
157
+ * provider or an older schema. When the merged document fails validation, the
158
+ * merge retries without those keys so one stale value cannot lock every later
159
+ * update; keys the patch itself sets are never pruned.
160
+ */
161
+ async #mergeSettings(stored, patch) {
162
+ const merged = { ...stored };
163
+ for (const [key, value] of Object.entries(patch)) if (value === null) delete merged[key];
164
+ else merged[key] = value;
165
+ this.#settingsSchema ??= normalizeFactorySandboxSettings(this.deps.sandbox);
166
+ const result = await this.#settingsSchema["~standard"].validate(merged);
167
+ if (!result.issues) return { merged: Object.keys(merged).length > 0 ? merged : null };
168
+ const declared = this.#describeSandbox().settingsSchema.properties ?? {};
169
+ const pruned = Object.fromEntries(Object.entries(merged).filter(([key]) => key in patch || Object.hasOwn(declared, key)));
170
+ if (Object.keys(pruned).length === Object.keys(merged).length) return { issues: result.issues };
171
+ if ((await this.#settingsSchema["~standard"].validate(pruned)).issues) return { issues: result.issues };
172
+ return { merged: Object.keys(pruned).length > 0 ? pruned : null };
173
+ }
116
174
  async #projects() {
117
175
  await this.deps.projects.ensureReady();
118
176
  return this.deps.projects;
@@ -173,6 +231,166 @@ var ProjectRoutes = class extends Route {
173
231
  repository
174
232
  };
175
233
  }
234
+ /** Every link of the project across every handle, with the handle that owns it, in position order. */
235
+ async #environmentLinks(orgId, projectId) {
236
+ const links = [];
237
+ for (const handle of await this.#handles()) for (const projectRepository of await handle.projectRepositories.listByProject({
238
+ orgId,
239
+ factoryProjectId: projectId
240
+ })) links.push({
241
+ handle,
242
+ projectRepository
243
+ });
244
+ return links.sort((a, b) => a.projectRepository.position - b.projectRepository.position || a.projectRepository.createdAt.getTime() - b.projectRepository.createdAt.getTime());
245
+ }
246
+ async #environmentPayload(orgId, project) {
247
+ const repositories = [];
248
+ for (const { handle, projectRepository } of await this.#environmentLinks(orgId, project.id)) {
249
+ const repository = await handle.repositories.get({
250
+ orgId,
251
+ id: projectRepository.repositoryId
252
+ });
253
+ repositories.push({
254
+ projectRepositoryId: projectRepository.id,
255
+ connectionId: projectRepository.connectionId,
256
+ repositoryId: projectRepository.repositoryId,
257
+ slug: repository?.slug ?? null,
258
+ defaultBranch: repository?.defaultBranch ?? null,
259
+ position: projectRepository.position,
260
+ inEnvironment: projectRepository.inEnvironment,
261
+ setupCommand: projectRepository.setupCommand,
262
+ teardownCommand: projectRepository.teardownCommand,
263
+ lastBuildStatus: projectRepository.lastBuildStatus,
264
+ lastBuildError: projectRepository.lastBuildError,
265
+ lastBuiltAt: projectRepository.lastBuiltAt
266
+ });
267
+ }
268
+ return { environment: {
269
+ sandbox: this.#describeSandbox(),
270
+ settings: project.sandboxSettings ?? {},
271
+ sandboxWorkingDirectory: project.sandboxWorkingDirectory,
272
+ workspaceSetupCommand: project.workspaceSetupCommand,
273
+ activeTemplateId: project.activeTemplateId,
274
+ activeTemplateHeads: project.activeTemplateHeads,
275
+ repositories,
276
+ ...await this.#buildPayload(project)
277
+ } };
278
+ }
279
+ /**
280
+ * The build keys of the environment payload, present only when the
281
+ * sandbox can build. Reads the project row and the schedule row; never
282
+ * the provider (live status is the builds routes' job).
283
+ */
284
+ async #buildPayload(project) {
285
+ const builds = this.deps.environmentBuilds;
286
+ if (!builds) return {};
287
+ const schedules = builds.scheduleAvailable ? builds.schedules() : void 0;
288
+ return {
289
+ buildTriggers: {
290
+ schedule: {
291
+ ...schedules ? await readSchedule(schedules, project.id) : {
292
+ enabled: false,
293
+ cron: null,
294
+ timezone: null
295
+ },
296
+ scheduleAvailable: builds.scheduleAvailable
297
+ },
298
+ push: {
299
+ enabled: project.buildOnPushEnabled,
300
+ debounceMinutes: project.buildPushDebounceMinutes
301
+ }
302
+ },
303
+ build: project.lastBuildId ? {
304
+ buildId: project.lastBuildId,
305
+ attemptedAt: project.lastBuildAttemptedAt?.toISOString() ?? null
306
+ } : null
307
+ };
308
+ }
309
+ /** The builds capability behind the routes, or the 404 to answer with. */
310
+ #builds(context) {
311
+ const builds = this.deps.sandbox?.builds;
312
+ const runner = this.deps.environmentBuilds;
313
+ if (!builds || !runner) return { response: context.json({ error: "no_builds" }, 404) };
314
+ return {
315
+ builds,
316
+ runner
317
+ };
318
+ }
319
+ #buildRoutes() {
320
+ return [
321
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.projectEnvironmentBuild.path, {
322
+ method: FACTORY_ROUTE_CONTRACTS.projectEnvironmentBuild.method,
323
+ requiresAuth: false,
324
+ handler: async (routeContext) => {
325
+ const context = loose(routeContext);
326
+ const tenant = await this.#resolveTenant(context);
327
+ if ("response" in tenant) return tenant.response;
328
+ const projectId = context.req.param("id");
329
+ if (!projectId || !UUID_RE.test(projectId)) return context.json({ error: "Project not found" }, 404);
330
+ const project = await this.#project(tenant.orgId, projectId);
331
+ if (!project) return context.json({ error: "Project not found" }, 404);
332
+ const capability = this.#builds(context);
333
+ if ("response" in capability) return capability.response;
334
+ const { heads: _heads, ...outcome } = await capability.runner.start(project.id, "manual");
335
+ return context.json(outcome);
336
+ }
337
+ }),
338
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.projectEnvironmentBuilds.path, {
339
+ method: FACTORY_ROUTE_CONTRACTS.projectEnvironmentBuilds.method,
340
+ requiresAuth: false,
341
+ handler: async (routeContext) => {
342
+ const context = loose(routeContext);
343
+ const tenant = await this.#resolveTenant(context);
344
+ if ("response" in tenant) return tenant.response;
345
+ const projectId = context.req.param("id");
346
+ if (!projectId || !UUID_RE.test(projectId)) return context.json({ error: "Project not found" }, 404);
347
+ const project = await this.#project(tenant.orgId, projectId);
348
+ if (!project) return context.json({ error: "Project not found" }, 404);
349
+ const capability = this.#builds(context);
350
+ if ("response" in capability) return capability.response;
351
+ if (!capability.builds.list) return context.json({ error: "no_history" }, 404);
352
+ const read = await capability.runner.readContext(project.id);
353
+ if (!read) return context.json({ error: "no_environment" }, 404);
354
+ const builds = await capability.builds.list(read.ctx, read.settings);
355
+ return context.json({ builds: builds.map(redactBuild) });
356
+ }
357
+ }),
358
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.projectEnvironmentBuildGet.path, {
359
+ method: FACTORY_ROUTE_CONTRACTS.projectEnvironmentBuildGet.method,
360
+ requiresAuth: false,
361
+ handler: async (routeContext) => {
362
+ const context = loose(routeContext);
363
+ const tenant = await this.#resolveTenant(context);
364
+ if ("response" in tenant) return tenant.response;
365
+ const projectId = context.req.param("id");
366
+ if (!projectId || !UUID_RE.test(projectId)) return context.json({ error: "Project not found" }, 404);
367
+ const project = await this.#project(tenant.orgId, projectId);
368
+ if (!project) return context.json({ error: "Project not found" }, 404);
369
+ const capability = this.#builds(context);
370
+ if ("response" in capability) return capability.response;
371
+ const buildId = context.req.param("buildId") ?? "";
372
+ if (!buildId) return context.json({ error: "Build not found" }, 404);
373
+ const read = await capability.runner.readContext(project.id);
374
+ if (!read) return context.json({ error: "no_environment" }, 404);
375
+ if (buildId !== project.lastBuildId) {
376
+ if (!(capability.builds.list ? await capability.builds.list(read.ctx, read.settings) : []).some((build) => build.buildId === buildId)) return context.json({ error: "Build not found" }, 404);
377
+ }
378
+ return context.json({ build: redactBuild(await capability.builds.get(read.ctx, read.settings, buildId)) });
379
+ }
380
+ })
381
+ ];
382
+ }
383
+ /** Best effort: a deleted project's cron schedule must not keep firing. */
384
+ async #dropSchedule(projectId) {
385
+ const runner = this.deps.environmentBuilds;
386
+ if (!runner?.scheduleAvailable) return;
387
+ try {
388
+ await runner.schedules().delete(scheduleIdFor(projectId));
389
+ } catch (error) {
390
+ if (error instanceof MastraError && error.id === "SCHEDULES_NOT_FOUND") return;
391
+ console.warn("[factory] could not delete the environment build schedule of a deleted project:", error);
392
+ }
393
+ }
176
394
  async #retireProjectRepositorySessions(handle, orgId, projectRepositoryId) {
177
395
  if ((await handle.sessions.listByProjectRepository({ projectRepositoryId })).length === 0) return true;
178
396
  if (!this.deps.sessionRetirement) return false;
@@ -372,6 +590,7 @@ var ProjectRoutes = class extends Route {
372
590
  orgId: tenant.orgId,
373
591
  id
374
592
  });
593
+ await this.#dropSchedule(id);
375
594
  return context.body(null, 204);
376
595
  }
377
596
  }),
@@ -508,6 +727,129 @@ var ProjectRoutes = class extends Route {
508
727
  return context.json({ projectRepository: await this.#repositoryPayload(found.handle, tenant.orgId, projectRepository) }, 201);
509
728
  }
510
729
  }),
730
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.projectEnvironmentGet.path, {
731
+ method: FACTORY_ROUTE_CONTRACTS.projectEnvironmentGet.method,
732
+ requiresAuth: false,
733
+ handler: async (routeContext) => {
734
+ const context = loose(routeContext);
735
+ const tenant = await this.#resolveTenant(context);
736
+ if ("response" in tenant) return tenant.response;
737
+ const projectId = context.req.param("id");
738
+ if (!projectId || !UUID_RE.test(projectId)) return context.json({ error: "Project not found" }, 404);
739
+ const project = await this.#project(tenant.orgId, projectId);
740
+ if (!project) return context.json({ error: "Project not found" }, 404);
741
+ return context.json(await this.#environmentPayload(tenant.orgId, project));
742
+ }
743
+ }),
744
+ registerApiRoute(FACTORY_ROUTE_CONTRACTS.projectEnvironmentUpdate.path, {
745
+ method: FACTORY_ROUTE_CONTRACTS.projectEnvironmentUpdate.method,
746
+ requiresAuth: false,
747
+ handler: async (routeContext) => {
748
+ const context = loose(routeContext);
749
+ const tenant = await this.#resolveTenant(context);
750
+ if ("response" in tenant) return tenant.response;
751
+ const projectId = context.req.param("id");
752
+ if (!projectId || !UUID_RE.test(projectId)) return context.json({ error: "Project not found" }, 404);
753
+ const project = await this.#project(tenant.orgId, projectId);
754
+ if (!project) return context.json({ error: "Project not found" }, 404);
755
+ const parsed = FACTORY_ROUTE_CONTRACTS.projectEnvironmentUpdate.bodySchema.safeParse(await readJson(context));
756
+ if (!parsed.success) return context.json({ error: "invalid_environment" }, 400);
757
+ const { repositories: repositoryPatches, settings: settingsPatch, buildTriggers, ...projectInput } = parsed.data;
758
+ const input = projectInput;
759
+ const runner = this.deps.environmentBuilds;
760
+ if (buildTriggers !== void 0) {
761
+ if (!runner) return context.json({ error: "no_builds" }, 400);
762
+ if (buildTriggers.push?.enabled !== void 0) input.buildOnPushEnabled = buildTriggers.push.enabled;
763
+ if (buildTriggers.push?.debounceMinutes !== void 0) input.buildPushDebounceMinutes = buildTriggers.push.debounceMinutes;
764
+ if (buildTriggers.schedule) if (!runner.scheduleAvailable) {
765
+ if (buildTriggers.schedule.enabled) return context.json({ error: "schedules_unavailable" }, 400);
766
+ } else {
767
+ const { cron, timezone } = buildTriggers.schedule;
768
+ const stored = await readSchedule(runner.schedules(), project.id);
769
+ const nextCron = cron ?? stored.cron;
770
+ if (!nextCron && buildTriggers.schedule.enabled) return context.json({
771
+ error: "invalid_environment",
772
+ issues: [{
773
+ message: "cron is required",
774
+ path: [
775
+ "buildTriggers",
776
+ "schedule",
777
+ "cron"
778
+ ]
779
+ }]
780
+ }, 400);
781
+ const nextTimezone = timezone ?? stored.timezone ?? void 0;
782
+ const message = nextCron ? invalidCronMessage(nextCron, nextTimezone) : void 0;
783
+ if (message) return context.json({
784
+ error: "invalid_environment",
785
+ issues: [{
786
+ message,
787
+ path: [
788
+ "buildTriggers",
789
+ "schedule",
790
+ "cron"
791
+ ]
792
+ }]
793
+ }, 400);
794
+ }
795
+ }
796
+ if (settingsPatch !== void 0 && !this.deps.sandbox) return context.json({ error: "no_sandbox" }, 400);
797
+ let updated = project;
798
+ if (Object.keys(input).length > 0 || settingsPatch !== void 0 || repositoryPatches !== void 0) {
799
+ let issues;
800
+ let repositoryOutcome;
801
+ const sourceControl = await this.#sourceControl();
802
+ const result = await (await this.#projects()).update({
803
+ orgId: tenant.orgId,
804
+ id: projectId,
805
+ input: async (current, ops) => {
806
+ let sandboxSettings;
807
+ if (settingsPatch !== void 0) {
808
+ const settings = await this.#mergeSettings(current.sandboxSettings ?? {}, settingsPatch);
809
+ if ("issues" in settings) {
810
+ issues = settings.issues;
811
+ return null;
812
+ }
813
+ sandboxSettings = settings.merged;
814
+ }
815
+ if (repositoryPatches !== void 0) {
816
+ repositoryOutcome = await sourceControl.updateEnvironmentRepositories(ops, {
817
+ factoryProjectId: current.id,
818
+ integrationIds: [...this.#versionControlIntegrationIds],
819
+ patches: repositoryPatches
820
+ });
821
+ if (repositoryOutcome !== "updated") return null;
822
+ }
823
+ return {
824
+ ...input,
825
+ ...sandboxSettings !== void 0 ? { sandboxSettings } : {}
826
+ };
827
+ }
828
+ });
829
+ if (repositoryOutcome === "not_found") return context.json({ error: "Project repository not found" }, 404);
830
+ if (repositoryOutcome === "incomplete_reorder") return context.json({ error: "invalid_environment" }, 400);
831
+ if (issues) return context.json({
832
+ error: "invalid_environment",
833
+ issues: issues.map((issue) => ({
834
+ message: issue.message,
835
+ path: Array.isArray(issue.path) ? issue.path.map((segment) => typeof segment === "object" && segment !== null && "key" in segment ? segment.key : segment) : []
836
+ }))
837
+ }, 400);
838
+ if (!result) return context.json({ error: "Project not found" }, 404);
839
+ updated = result;
840
+ }
841
+ if (buildTriggers?.schedule && runner?.scheduleAvailable) {
842
+ const stored = await readSchedule(runner.schedules(), project.id);
843
+ const cron = buildTriggers.schedule.cron ?? stored.cron;
844
+ if (cron) await ensureSchedule(runner.schedules(), project.id, {
845
+ enabled: buildTriggers.schedule.enabled,
846
+ cron,
847
+ ...buildTriggers.schedule.timezone ?? stored.timezone ? { timezone: buildTriggers.schedule.timezone ?? stored.timezone } : {}
848
+ });
849
+ }
850
+ return context.json(await this.#environmentPayload(tenant.orgId, updated));
851
+ }
852
+ }),
511
853
  registerApiRoute("/web/factory/projects/:id/repositories/:projectRepositoryId", {
512
854
  method: "PATCH",
513
855
  requiresAuth: false,
@@ -557,7 +899,8 @@ var ProjectRoutes = class extends Route {
557
899
  });
558
900
  return context.body(null, 204);
559
901
  }
560
- })
902
+ }),
903
+ ...this.#buildRoutes()
561
904
  ];
562
905
  }
563
906
  };