@studio-foundation/api 0.3.0-beta.1 → 0.3.0-beta.6

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 (62) hide show
  1. package/package.json +7 -4
  2. package/ARCHITECTURE.md +0 -52
  3. package/src/.gitkeep +0 -0
  4. package/src/api.ts +0 -8
  5. package/src/bootstrap.ts +0 -259
  6. package/src/event-bus.ts +0 -64
  7. package/src/index.ts +0 -58
  8. package/src/integration-runtime.ts +0 -180
  9. package/src/integration-store.ts +0 -125
  10. package/src/integrations/linear/failure-handler.ts +0 -93
  11. package/src/integrations/linear/webhook-handler.ts +0 -156
  12. package/src/integrations/registry.ts +0 -12
  13. package/src/integrations/types.ts +0 -37
  14. package/src/launcher.ts +0 -214
  15. package/src/logger.ts +0 -50
  16. package/src/plans.ts +0 -43
  17. package/src/routes/agents.ts +0 -131
  18. package/src/routes/config.ts +0 -154
  19. package/src/routes/contracts.ts +0 -205
  20. package/src/routes/pipelines.ts +0 -146
  21. package/src/routes/projects.ts +0 -237
  22. package/src/routes/runs.ts +0 -468
  23. package/src/routes/skills.ts +0 -136
  24. package/src/routes/tools.ts +0 -222
  25. package/src/routes/users.ts +0 -169
  26. package/src/routes/validate.ts +0 -196
  27. package/src/routes/webhooks.ts +0 -120
  28. package/src/server.ts +0 -155
  29. package/src/spawners/http-api-spawner.ts +0 -96
  30. package/src/user-store-pg.ts +0 -138
  31. package/src/user-store.ts +0 -125
  32. package/src/utils/repo-resolver.ts +0 -3
  33. package/src/webhook-dispatcher.ts +0 -142
  34. package/src/webhook-store.ts +0 -141
  35. package/tests/agents.test.ts +0 -164
  36. package/tests/cancel.test.ts +0 -120
  37. package/tests/config.test.ts +0 -196
  38. package/tests/contracts.test.ts +0 -302
  39. package/tests/event-bus.test.ts +0 -53
  40. package/tests/http-api-spawner.test.ts +0 -158
  41. package/tests/integration-runtime.test.ts +0 -199
  42. package/tests/integration-store.test.ts +0 -66
  43. package/tests/integrations/linear/failure-handler.test.ts +0 -113
  44. package/tests/integrations/linear/webhook-handler.test.ts +0 -191
  45. package/tests/launcher.test.ts +0 -380
  46. package/tests/linear-webhook.test.ts +0 -390
  47. package/tests/pipelines.test.ts +0 -166
  48. package/tests/projects.test.ts +0 -283
  49. package/tests/runs.test.ts +0 -379
  50. package/tests/server.test.ts +0 -208
  51. package/tests/skills.test.ts +0 -149
  52. package/tests/sse.test.ts +0 -129
  53. package/tests/tools.test.ts +0 -233
  54. package/tests/user-store.test.ts +0 -93
  55. package/tests/users.test.ts +0 -189
  56. package/tests/utils/repo-resolver.test.ts +0 -105
  57. package/tests/validate.test.ts +0 -233
  58. package/tests/webhook-dispatcher.test.ts +0 -214
  59. package/tests/webhook-store.test.ts +0 -98
  60. package/tests/webhooks.test.ts +0 -176
  61. package/tsconfig.json +0 -24
  62. package/vitest.config.ts +0 -7
package/src/plans.ts DELETED
@@ -1,43 +0,0 @@
1
- // api/src/plans.ts
2
- // Plan configuration types and default plans
3
-
4
- export interface PlanLimits {
5
- runs_per_day: number; // -1 = unlimited
6
- max_concurrent: number;
7
- max_tokens_per_run: number; // -1 = unlimited
8
- rate_limit_per_minute: number;
9
- }
10
-
11
- export type PlansConfig = Record<string, PlanLimits>;
12
-
13
- export const DEFAULT_PLANS: PlansConfig = {
14
- free: {
15
- runs_per_day: 5,
16
- max_concurrent: 1,
17
- max_tokens_per_run: 50_000,
18
- rate_limit_per_minute: 10,
19
- },
20
- pro: {
21
- runs_per_day: 100,
22
- max_concurrent: 5,
23
- max_tokens_per_run: 500_000,
24
- rate_limit_per_minute: 60,
25
- },
26
- unlimited: {
27
- runs_per_day: -1,
28
- max_concurrent: 20,
29
- max_tokens_per_run: -1,
30
- rate_limit_per_minute: 300,
31
- },
32
- };
33
-
34
- /** Merge user-supplied partial plans with defaults. Unknown plans fall back to 'free'. */
35
- export function resolvePlans(configPlans?: Record<string, PlanLimits>): PlansConfig {
36
- if (!configPlans) return DEFAULT_PLANS;
37
- return { ...DEFAULT_PLANS, ...configPlans };
38
- }
39
-
40
- /** Get plan limits for a user, defaulting to 'free' if plan is unknown. */
41
- export function getPlanLimits(plans: PlansConfig, planName: string): PlanLimits {
42
- return plans[planName] ?? plans.free ?? DEFAULT_PLANS.free;
43
- }
@@ -1,131 +0,0 @@
1
- import { readdir, readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import * as yaml from 'js-yaml';
4
- import type { FastifyInstance } from 'fastify';
5
- import type { ServerDeps } from '../server.js';
6
-
7
- export async function agentsRoutes(
8
- fastify: FastifyInstance,
9
- options: { deps: ServerDeps }
10
- ): Promise<void> {
11
- const agentsDir = join(options.deps.configsDir, 'agents');
12
-
13
- const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
14
-
15
- // GET /api/agents
16
- fastify.get('/agents', {
17
- schema: {
18
- tags: ['agents'],
19
- summary: 'List all agent names',
20
- response: {
21
- 200: {
22
- type: 'object',
23
- properties: {
24
- agents: { type: 'array', items: { type: 'string' } },
25
- },
26
- },
27
- },
28
- },
29
- }, async (_request, reply) => {
30
- let entries: string[];
31
- try {
32
- entries = await readdir(agentsDir);
33
- } catch {
34
- return reply.send({ agents: [] });
35
- }
36
- const agents = entries
37
- .filter(f => f.endsWith('.agent.yaml'))
38
- .map(f => f.slice(0, -'.agent.yaml'.length));
39
- return reply.send({ agents });
40
- });
41
-
42
- // GET /api/agents/:name
43
- fastify.get<{ Params: { name: string } }>('/agents/:name', {
44
- schema: {
45
- tags: ['agents'],
46
- summary: 'Get an agent by name',
47
- params: {
48
- type: 'object',
49
- properties: { name: { type: 'string' } },
50
- required: ['name'],
51
- },
52
- response: {
53
- 200: { type: 'object', additionalProperties: true },
54
- 404: errorSchema,
55
- },
56
- },
57
- }, async (request, reply) => {
58
- const filePath = join(agentsDir, `${request.params.name}.agent.yaml`);
59
- let content: string;
60
- try {
61
- content = await readFile(filePath, 'utf-8');
62
- } catch {
63
- return reply.status(404).send({ error: 'Agent not found' });
64
- }
65
- return reply.send(yaml.load(content));
66
- });
67
-
68
- // PUT /api/agents/:name
69
- fastify.put<{
70
- Params: { name: string };
71
- Body: Record<string, unknown>;
72
- }>('/agents/:name', {
73
- schema: {
74
- tags: ['agents'],
75
- summary: 'Create or update an agent',
76
- params: {
77
- type: 'object',
78
- properties: { name: { type: 'string' } },
79
- required: ['name'],
80
- },
81
- body: { type: 'object' },
82
- response: {
83
- 200: {
84
- type: 'object',
85
- properties: {
86
- name: { type: 'string' },
87
- content: { type: 'object', additionalProperties: true },
88
- },
89
- },
90
- 400: errorSchema,
91
- },
92
- },
93
- }, async (request, reply) => {
94
- const body = request.body;
95
-
96
- if (!body['name'] || typeof body['name'] !== 'string') {
97
- return reply.status(400).send({ error: "Agent must have a 'name' field (string)" });
98
- }
99
-
100
- await mkdir(agentsDir, { recursive: true });
101
- const filePath = join(agentsDir, `${request.params.name}.agent.yaml`);
102
- await writeFile(filePath, yaml.dump(body), 'utf-8');
103
-
104
- return reply.send({ name: request.params.name, content: body });
105
- });
106
-
107
- // DELETE /api/agents/:name
108
- fastify.delete<{ Params: { name: string } }>('/agents/:name', {
109
- schema: {
110
- tags: ['agents'],
111
- summary: 'Delete an agent',
112
- params: {
113
- type: 'object',
114
- properties: { name: { type: 'string' } },
115
- required: ['name'],
116
- },
117
- response: {
118
- 204: { type: 'null', description: 'No content' },
119
- 404: errorSchema,
120
- },
121
- },
122
- }, async (request, reply) => {
123
- const filePath = join(agentsDir, `${request.params.name}.agent.yaml`);
124
- try {
125
- await unlink(filePath);
126
- } catch {
127
- return reply.status(404).send({ error: 'Agent not found' });
128
- }
129
- return reply.status(204).send();
130
- });
131
- }
@@ -1,154 +0,0 @@
1
- import { readFile, writeFile, mkdir } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import * as yaml from 'js-yaml';
4
- import type { FastifyInstance } from 'fastify';
5
- import type { ServerDeps } from '../server.js';
6
-
7
- const KNOWN_PROVIDERS = new Set(['anthropic', 'openai', 'ollama']);
8
-
9
- interface RawConfig {
10
- providers?: Record<string, { apiKey?: string }>;
11
- defaults?: Record<string, unknown>;
12
- }
13
-
14
- async function readConfig(configsDir: string): Promise<RawConfig> {
15
- try {
16
- const content = await readFile(join(configsDir, 'config.yaml'), 'utf-8');
17
- return (yaml.load(content) as RawConfig) ?? {};
18
- } catch {
19
- return {};
20
- }
21
- }
22
-
23
- async function writeConfig(configsDir: string, config: RawConfig): Promise<void> {
24
- await mkdir(configsDir, { recursive: true });
25
- await writeFile(join(configsDir, 'config.yaml'), yaml.dump(config), 'utf-8');
26
- }
27
-
28
- function maskConfig(config: RawConfig): RawConfig {
29
- if (!config.providers) return config;
30
- const maskedProviders: Record<string, { apiKey: string }> = {};
31
- for (const [name, provider] of Object.entries(config.providers)) {
32
- maskedProviders[name] = { apiKey: provider.apiKey ? '***' : '' };
33
- }
34
- return { ...config, providers: maskedProviders };
35
- }
36
-
37
- export async function configRoutes(
38
- fastify: FastifyInstance,
39
- options: { deps: ServerDeps }
40
- ): Promise<void> {
41
- const { configsDir } = options.deps;
42
-
43
- const configResponseSchema = {
44
- type: 'object',
45
- properties: {
46
- providers: {
47
- type: 'object',
48
- additionalProperties: {
49
- type: 'object',
50
- properties: { apiKey: { type: 'string' } },
51
- },
52
- },
53
- defaults: {
54
- type: 'object',
55
- properties: {
56
- provider: { type: 'string' },
57
- model: { type: 'string' },
58
- },
59
- },
60
- },
61
- };
62
-
63
- // GET /api/config
64
- fastify.get('/config', {
65
- schema: {
66
- tags: ['config'],
67
- summary: 'Get current config',
68
- response: {
69
- 200: configResponseSchema,
70
- },
71
- },
72
- }, async (_request, reply) => {
73
- const config = await readConfig(configsDir);
74
- return reply.send({
75
- providers: {},
76
- ...maskConfig(config),
77
- });
78
- });
79
-
80
- // PATCH /api/config
81
- fastify.patch<{ Body: Partial<RawConfig> }>('/config', {
82
- schema: {
83
- tags: ['config'],
84
- summary: 'Patch config (merge defaults and providers)',
85
- body: { type: 'object' },
86
- response: {
87
- 200: configResponseSchema,
88
- },
89
- },
90
- }, async (request, reply) => {
91
- const config = await readConfig(configsDir);
92
- const patch = request.body;
93
-
94
- if (patch.defaults) {
95
- config.defaults = { ...config.defaults, ...patch.defaults };
96
- }
97
- if (patch.providers) {
98
- config.providers = { ...config.providers, ...patch.providers };
99
- }
100
-
101
- await writeConfig(configsDir, config);
102
- return reply.send({
103
- providers: {},
104
- ...maskConfig(config),
105
- });
106
- });
107
-
108
- // POST /api/config/providers
109
- fastify.post<{ Body: { provider: string; apiKeyEnvVar: string } }>(
110
- '/config/providers',
111
- {
112
- schema: {
113
- tags: ['config'],
114
- summary: 'Add or update a provider',
115
- body: {
116
- type: 'object',
117
- required: ['provider', 'apiKeyEnvVar'],
118
- properties: {
119
- provider: { type: 'string' },
120
- apiKeyEnvVar: { type: 'string' },
121
- },
122
- },
123
- response: {
124
- 200: configResponseSchema,
125
- 400: {
126
- type: 'object',
127
- properties: { error: { type: 'string' } },
128
- },
129
- },
130
- },
131
- },
132
- async (request, reply) => {
133
- const { provider, apiKeyEnvVar } = request.body;
134
-
135
- if (!KNOWN_PROVIDERS.has(provider)) {
136
- return reply.status(400).send({
137
- error: `Unknown provider: '${provider}'. Supported: ${[...KNOWN_PROVIDERS].join(', ')}`,
138
- });
139
- }
140
-
141
- const config = await readConfig(configsDir);
142
- config.providers = {
143
- ...config.providers,
144
- [provider]: { apiKey: `\${${apiKeyEnvVar}}` },
145
- };
146
-
147
- await writeConfig(configsDir, config);
148
- return reply.send({
149
- providers: {},
150
- ...maskConfig(config),
151
- });
152
- }
153
- );
154
- }
@@ -1,205 +0,0 @@
1
- import { readdir, readFile, writeFile, unlink, mkdir } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import * as yaml from 'js-yaml';
4
- import type { FastifyInstance } from 'fastify';
5
- import type { ServerDeps } from '../server.js';
6
- import { loadContract, validateOutput } from '@studio-foundation/engine';
7
- import type { ToolCall } from '@studio-foundation/contracts';
8
-
9
- export async function contractsRoutes(
10
- fastify: FastifyInstance,
11
- options: { deps: ServerDeps }
12
- ): Promise<void> {
13
- const contractsDir = join(options.deps.configsDir, 'contracts');
14
- const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
15
-
16
- // GET /api/contracts
17
- fastify.get('/contracts', {
18
- schema: {
19
- tags: ['contracts'],
20
- summary: 'List all contract names',
21
- response: {
22
- 200: {
23
- type: 'object',
24
- properties: {
25
- contracts: { type: 'array', items: { type: 'string' } },
26
- },
27
- },
28
- },
29
- },
30
- }, async (_request, reply) => {
31
- let entries: string[];
32
- try {
33
- entries = await readdir(contractsDir);
34
- } catch {
35
- return reply.send({ contracts: [] });
36
- }
37
- const contracts = entries
38
- .filter(f => f.endsWith('.contract.yaml'))
39
- .map(f => f.slice(0, -'.contract.yaml'.length));
40
- return reply.send({ contracts });
41
- });
42
-
43
- // GET /api/contracts/:name
44
- fastify.get<{ Params: { name: string } }>('/contracts/:name', {
45
- schema: {
46
- tags: ['contracts'],
47
- summary: 'Get a contract by name',
48
- params: {
49
- type: 'object',
50
- properties: { name: { type: 'string' } },
51
- required: ['name'],
52
- },
53
- response: {
54
- 200: { type: 'object', additionalProperties: true },
55
- 404: errorSchema,
56
- },
57
- },
58
- }, async (request, reply) => {
59
- const filePath = join(contractsDir, `${request.params.name}.contract.yaml`);
60
- let content: string;
61
- try {
62
- content = await readFile(filePath, 'utf-8');
63
- } catch {
64
- return reply.status(404).send({ error: 'Contract not found' });
65
- }
66
- return reply.send(yaml.load(content));
67
- });
68
-
69
- // PUT /api/contracts/:name
70
- fastify.put<{
71
- Params: { name: string };
72
- Body: Record<string, unknown>;
73
- }>('/contracts/:name', {
74
- schema: {
75
- tags: ['contracts'],
76
- summary: 'Create or update a contract',
77
- params: {
78
- type: 'object',
79
- properties: { name: { type: 'string' } },
80
- required: ['name'],
81
- },
82
- body: { type: 'object' },
83
- response: {
84
- 200: {
85
- type: 'object',
86
- properties: {
87
- name: { type: 'string' },
88
- content: { type: 'object', additionalProperties: true },
89
- },
90
- },
91
- 400: errorSchema,
92
- },
93
- },
94
- }, async (request, reply) => {
95
- const body = request.body;
96
-
97
- if (!body['name'] || typeof body['name'] !== 'string') {
98
- return reply.status(400).send({ error: "Contract must have a 'name' field (string)" });
99
- }
100
- if (body['version'] === undefined) {
101
- return reply.status(400).send({ error: "Contract must have a 'version' field" });
102
- }
103
-
104
- await mkdir(contractsDir, { recursive: true });
105
- const filePath = join(contractsDir, `${request.params.name}.contract.yaml`);
106
- await writeFile(filePath, yaml.dump(body), 'utf-8');
107
-
108
- return reply.send({ name: request.params.name, content: body });
109
- });
110
-
111
- // DELETE /api/contracts/:name
112
- fastify.delete<{ Params: { name: string } }>('/contracts/:name', {
113
- schema: {
114
- tags: ['contracts'],
115
- summary: 'Delete a contract',
116
- params: {
117
- type: 'object',
118
- properties: { name: { type: 'string' } },
119
- required: ['name'],
120
- },
121
- response: {
122
- 204: { type: 'null', description: 'No content' },
123
- 404: errorSchema,
124
- },
125
- },
126
- }, async (request, reply) => {
127
- const filePath = join(contractsDir, `${request.params.name}.contract.yaml`);
128
- try {
129
- await unlink(filePath);
130
- } catch {
131
- return reply.status(404).send({ error: 'Contract not found' });
132
- }
133
- return reply.status(204).send();
134
- });
135
-
136
- // POST /api/contracts/:name/validate
137
- fastify.post<{
138
- Params: { name: string };
139
- Body: { output: unknown; tool_calls?: ToolCall[] };
140
- }>('/contracts/:name/validate', {
141
- schema: {
142
- tags: ['contracts'],
143
- summary: 'Validate an output against a contract without running a pipeline',
144
- params: {
145
- type: 'object',
146
- properties: { name: { type: 'string' } },
147
- required: ['name'],
148
- },
149
- body: {
150
- type: 'object',
151
- required: ['output'],
152
- properties: {
153
- output: { type: 'object', additionalProperties: true },
154
- tool_calls: {
155
- type: 'array',
156
- items: {
157
- type: 'object',
158
- properties: {
159
- id: { type: 'string' },
160
- name: { type: 'string' },
161
- arguments: { type: 'object', additionalProperties: true },
162
- result: {},
163
- error: { type: 'string' },
164
- },
165
- required: ['id', 'name'],
166
- },
167
- },
168
- },
169
- },
170
- response: {
171
- 200: {
172
- type: 'object',
173
- required: ['valid', 'errors', 'warnings', 'post_validation'],
174
- properties: {
175
- valid: { type: 'boolean' },
176
- errors: { type: 'array', items: { type: 'string' } },
177
- warnings: { type: 'array', items: { type: 'string' } },
178
- post_validation: {
179
- type: 'object',
180
- required: ['accepted'],
181
- properties: {
182
- accepted: { type: 'boolean' },
183
- rejection_reason: { type: 'string' },
184
- rejection_details: { type: 'array', items: { type: 'string' } },
185
- },
186
- },
187
- },
188
- },
189
- 400: errorSchema,
190
- 404: errorSchema,
191
- },
192
- },
193
- }, async (request, reply) => {
194
- let contract;
195
- try {
196
- contract = await loadContract(request.params.name, contractsDir);
197
- } catch {
198
- return reply.status(404).send({ error: 'Contract not found' });
199
- }
200
-
201
- const { output, tool_calls = [] } = request.body;
202
- const result = validateOutput(contract, output, tool_calls);
203
- return reply.send(result);
204
- });
205
- }
@@ -1,146 +0,0 @@
1
- import { readdir, readFile, writeFile, unlink } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import yaml from 'js-yaml';
4
- import type { FastifyInstance } from 'fastify';
5
- import type { ServerDeps } from '../server.js';
6
-
7
- function pipelinePath(configsDir: string, name: string): string {
8
- return join(configsDir, 'pipelines', `${name}.pipeline.yaml`);
9
- }
10
-
11
- export async function pipelinesRoutes(
12
- fastify: FastifyInstance,
13
- options: { deps: ServerDeps }
14
- ): Promise<void> {
15
- const { configsDir } = options.deps;
16
-
17
- // GET /api/pipelines — list all pipeline names
18
- fastify.get('/pipelines', {
19
- schema: {
20
- tags: ['pipelines'],
21
- summary: 'List all pipeline names',
22
- response: {
23
- 200: {
24
- type: 'object',
25
- properties: {
26
- pipelines: { type: 'array', items: { type: 'string' } },
27
- },
28
- },
29
- },
30
- },
31
- }, async (_request, reply) => {
32
- const pipelinesDir = join(configsDir, 'pipelines');
33
- let entries: string[];
34
- try {
35
- entries = await readdir(pipelinesDir);
36
- } catch {
37
- return reply.send({ pipelines: [] });
38
- }
39
- const pipelines = entries
40
- .filter(f => f.endsWith('.pipeline.yaml'))
41
- .map(f => f.replace('.pipeline.yaml', ''));
42
- return reply.send({ pipelines });
43
- });
44
-
45
- const errorSchema = { type: 'object', properties: { error: { type: 'string' } } };
46
-
47
- // GET /api/pipelines/:name — read a pipeline (YAML parsed to JSON)
48
- fastify.get<{ Params: { name: string } }>('/pipelines/:name', {
49
- schema: {
50
- tags: ['pipelines'],
51
- summary: 'Get a pipeline by name',
52
- params: {
53
- type: 'object',
54
- properties: { name: { type: 'string' } },
55
- required: ['name'],
56
- },
57
- response: {
58
- 200: { type: 'object', additionalProperties: true },
59
- 404: errorSchema,
60
- },
61
- },
62
- }, async (request, reply) => {
63
- const { name } = request.params;
64
- let content: string;
65
- try {
66
- content = await readFile(pipelinePath(configsDir, name), 'utf-8');
67
- } catch {
68
- return reply.status(404).send({ error: `Pipeline '${name}' not found` });
69
- }
70
- const parsed = yaml.load(content);
71
- return reply.send(parsed);
72
- });
73
-
74
- // PUT /api/pipelines/:name — create or update a pipeline (YAML or JSON body)
75
- fastify.put<{ Params: { name: string }; Body: unknown }>(
76
- '/pipelines/:name',
77
- {
78
- schema: {
79
- tags: ['pipelines'],
80
- summary: 'Create or update a pipeline (YAML text or JSON body)',
81
- params: {
82
- type: 'object',
83
- properties: { name: { type: 'string' } },
84
- required: ['name'],
85
- },
86
- body: { oneOf: [{ type: 'object', additionalProperties: true }, { type: 'string' }] },
87
- response: {
88
- 200: {
89
- type: 'object',
90
- properties: { name: { type: 'string' } },
91
- },
92
- 400: errorSchema,
93
- },
94
- },
95
- },
96
- async (request, reply) => {
97
- const { name } = request.params;
98
- const contentType = request.headers['content-type'] ?? '';
99
-
100
- let yamlContent: string;
101
- if (contentType.includes('application/json')) {
102
- yamlContent = yaml.dump(request.body);
103
- } else {
104
- yamlContent = request.body as string;
105
- }
106
-
107
- try {
108
- yaml.load(yamlContent);
109
- } catch (err) {
110
- const message = err instanceof Error ? err.message : 'Invalid YAML';
111
- return reply.status(400).send({ error: message });
112
- }
113
-
114
- await writeFile(pipelinePath(configsDir, name), yamlContent, 'utf-8');
115
- return reply.send({ name });
116
- }
117
- );
118
-
119
- // DELETE /api/pipelines/:name — delete a pipeline
120
- fastify.delete<{ Params: { name: string } }>('/pipelines/:name', {
121
- schema: {
122
- tags: ['pipelines'],
123
- summary: 'Delete a pipeline',
124
- params: {
125
- type: 'object',
126
- properties: { name: { type: 'string' } },
127
- required: ['name'],
128
- },
129
- response: {
130
- 200: {
131
- type: 'object',
132
- properties: { deleted: { type: 'string' } },
133
- },
134
- 404: errorSchema,
135
- },
136
- },
137
- }, async (request, reply) => {
138
- const { name } = request.params;
139
- try {
140
- await unlink(pipelinePath(configsDir, name));
141
- } catch {
142
- return reply.status(404).send({ error: `Pipeline '${name}' not found` });
143
- }
144
- return reply.send({ deleted: name });
145
- });
146
- }