@sourcepress/server 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.omc/state/agent-replay-31d84b63-606a-4368-b9e6-93fe4f5ae0f7.jsonl +2 -0
- package/.omc/state/last-tool-error.json +7 -0
- package/.omc/state/mission-state.json +53 -0
- package/.omc/state/subagent-tracking.json +17 -0
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +26 -0
- package/dist/__tests__/app-integration.test.d.ts +2 -0
- package/dist/__tests__/app-integration.test.d.ts.map +1 -0
- package/dist/__tests__/app-integration.test.js +71 -0
- package/dist/__tests__/app-integration.test.js.map +1 -0
- package/dist/__tests__/approval.test.d.ts +2 -0
- package/dist/__tests__/approval.test.d.ts.map +1 -0
- package/dist/__tests__/approval.test.js +170 -0
- package/dist/__tests__/approval.test.js.map +1 -0
- package/dist/__tests__/content.test.d.ts +2 -0
- package/dist/__tests__/content.test.d.ts.map +1 -0
- package/dist/__tests__/content.test.js +187 -0
- package/dist/__tests__/content.test.js.map +1 -0
- package/dist/__tests__/engine.test.d.ts +2 -0
- package/dist/__tests__/engine.test.d.ts.map +1 -0
- package/dist/__tests__/engine.test.js +77 -0
- package/dist/__tests__/engine.test.js.map +1 -0
- package/dist/__tests__/eval.test.d.ts +2 -0
- package/dist/__tests__/eval.test.d.ts.map +1 -0
- package/dist/__tests__/eval.test.js +320 -0
- package/dist/__tests__/eval.test.js.map +1 -0
- package/dist/__tests__/graph.test.d.ts +2 -0
- package/dist/__tests__/graph.test.d.ts.map +1 -0
- package/dist/__tests__/graph.test.js +169 -0
- package/dist/__tests__/graph.test.js.map +1 -0
- package/dist/__tests__/health.test.d.ts +2 -0
- package/dist/__tests__/health.test.d.ts.map +1 -0
- package/dist/__tests__/health.test.js +56 -0
- package/dist/__tests__/health.test.js.map +1 -0
- package/dist/__tests__/import.test.d.ts +2 -0
- package/dist/__tests__/import.test.d.ts.map +1 -0
- package/dist/__tests__/import.test.js +138 -0
- package/dist/__tests__/import.test.js.map +1 -0
- package/dist/__tests__/intent.test.d.ts +2 -0
- package/dist/__tests__/intent.test.d.ts.map +1 -0
- package/dist/__tests__/intent.test.js +122 -0
- package/dist/__tests__/intent.test.js.map +1 -0
- package/dist/__tests__/jobs.test.d.ts +2 -0
- package/dist/__tests__/jobs.test.d.ts.map +1 -0
- package/dist/__tests__/jobs.test.js +96 -0
- package/dist/__tests__/jobs.test.js.map +1 -0
- package/dist/__tests__/knowledge.test.d.ts +2 -0
- package/dist/__tests__/knowledge.test.d.ts.map +1 -0
- package/dist/__tests__/knowledge.test.js +110 -0
- package/dist/__tests__/knowledge.test.js.map +1 -0
- package/dist/__tests__/media-routes.test.d.ts +2 -0
- package/dist/__tests__/media-routes.test.d.ts.map +1 -0
- package/dist/__tests__/media-routes.test.js +88 -0
- package/dist/__tests__/media-routes.test.js.map +1 -0
- package/dist/__tests__/schema.test.d.ts +2 -0
- package/dist/__tests__/schema.test.d.ts.map +1 -0
- package/dist/__tests__/schema.test.js +92 -0
- package/dist/__tests__/schema.test.js.map +1 -0
- package/dist/app.d.ts +7 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +85 -0
- package/dist/app.js.map +1 -0
- package/dist/engine.d.ts +38 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +106 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +17 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +54 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +2 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +13 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +24 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +30 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/index.d.ts +7 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +5 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/rate-limit.d.ts +11 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +26 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/route-error-handler.d.ts +12 -0
- package/dist/middleware/route-error-handler.d.ts.map +1 -0
- package/dist/middleware/route-error-handler.js +9 -0
- package/dist/middleware/route-error-handler.js.map +1 -0
- package/dist/routes/approval.d.ts +4 -0
- package/dist/routes/approval.d.ts.map +1 -0
- package/dist/routes/approval.js +70 -0
- package/dist/routes/approval.js.map +1 -0
- package/dist/routes/content.d.ts +4 -0
- package/dist/routes/content.d.ts.map +1 -0
- package/dist/routes/content.js +145 -0
- package/dist/routes/content.js.map +1 -0
- package/dist/routes/eval.d.ts +4 -0
- package/dist/routes/eval.d.ts.map +1 -0
- package/dist/routes/eval.js +178 -0
- package/dist/routes/eval.js.map +1 -0
- package/dist/routes/graph.d.ts +4 -0
- package/dist/routes/graph.d.ts.map +1 -0
- package/dist/routes/graph.js +90 -0
- package/dist/routes/graph.js.map +1 -0
- package/dist/routes/health.d.ts +13 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +19 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/import.d.ts +4 -0
- package/dist/routes/import.d.ts.map +1 -0
- package/dist/routes/import.js +85 -0
- package/dist/routes/import.js.map +1 -0
- package/dist/routes/index.d.ts +12 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/intent.d.ts +4 -0
- package/dist/routes/intent.d.ts.map +1 -0
- package/dist/routes/intent.js +80 -0
- package/dist/routes/intent.js.map +1 -0
- package/dist/routes/jobs.d.ts +4 -0
- package/dist/routes/jobs.d.ts.map +1 -0
- package/dist/routes/jobs.js +67 -0
- package/dist/routes/jobs.js.map +1 -0
- package/dist/routes/knowledge.d.ts +4 -0
- package/dist/routes/knowledge.d.ts.map +1 -0
- package/dist/routes/knowledge.js +48 -0
- package/dist/routes/knowledge.js.map +1 -0
- package/dist/routes/media.d.ts +4 -0
- package/dist/routes/media.d.ts.map +1 -0
- package/dist/routes/media.js +87 -0
- package/dist/routes/media.js.map +1 -0
- package/dist/routes/schema.d.ts +4 -0
- package/dist/routes/schema.d.ts.map +1 -0
- package/dist/routes/schema.js +54 -0
- package/dist/routes/schema.js.map +1 -0
- package/dist/standalone.d.ts +2 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +115 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +36 -0
- package/src/__tests__/app-integration.test.ts +80 -0
- package/src/__tests__/approval.test.ts +195 -0
- package/src/__tests__/content.test.ts +202 -0
- package/src/__tests__/engine.test.ts +86 -0
- package/src/__tests__/eval.test.ts +343 -0
- package/src/__tests__/graph.test.ts +182 -0
- package/src/__tests__/health.test.ts +68 -0
- package/src/__tests__/import.test.ts +148 -0
- package/src/__tests__/intent.test.ts +133 -0
- package/src/__tests__/jobs.test.ts +107 -0
- package/src/__tests__/knowledge.test.ts +121 -0
- package/src/__tests__/media-routes.test.ts +109 -0
- package/src/__tests__/schema.test.ts +100 -0
- package/src/app.ts +92 -0
- package/src/engine.ts +168 -0
- package/src/index.ts +31 -0
- package/src/middleware/auth.ts +66 -0
- package/src/middleware/cors.ts +15 -0
- package/src/middleware/error-handler.ts +42 -0
- package/src/middleware/index.ts +6 -0
- package/src/middleware/rate-limit.ts +27 -0
- package/src/middleware/route-error-handler.ts +13 -0
- package/src/routes/approval.ts +90 -0
- package/src/routes/content.ts +256 -0
- package/src/routes/eval.ts +262 -0
- package/src/routes/graph.ts +111 -0
- package/src/routes/health.ts +33 -0
- package/src/routes/import.ts +122 -0
- package/src/routes/index.ts +11 -0
- package/src/routes/intent.ts +105 -0
- package/src/routes/jobs.ts +84 -0
- package/src/routes/knowledge.ts +73 -0
- package/src/routes/media.ts +117 -0
- package/src/routes/schema.ts +75 -0
- package/src/standalone.ts +130 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { cors } from "hono/cors";
|
|
2
|
+
|
|
3
|
+
// Default to localhost only when no origins are configured (deny-by-default in production)
|
|
4
|
+
const DEFAULT_ORIGINS = ["http://localhost:3000", "http://localhost:4321", "http://localhost:5173"];
|
|
5
|
+
|
|
6
|
+
export function corsMiddleware(origins?: string | string[]) {
|
|
7
|
+
const origin = origins ?? DEFAULT_ORIGINS;
|
|
8
|
+
|
|
9
|
+
return cors({
|
|
10
|
+
origin,
|
|
11
|
+
allowMethods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
|
12
|
+
allowHeaders: ["Content-Type", "Authorization"],
|
|
13
|
+
maxAge: 86400,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Context, Next } from "hono";
|
|
2
|
+
import { handleRouteError } from "./route-error-handler.js";
|
|
3
|
+
|
|
4
|
+
export interface ApiError {
|
|
5
|
+
status: number;
|
|
6
|
+
message: string;
|
|
7
|
+
code: string;
|
|
8
|
+
details?: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class SourcePressError extends Error {
|
|
12
|
+
status: number;
|
|
13
|
+
code: string;
|
|
14
|
+
details?: unknown;
|
|
15
|
+
|
|
16
|
+
constructor(status: number, code: string, message: string, details?: unknown) {
|
|
17
|
+
super(message);
|
|
18
|
+
this.name = "SourcePressError";
|
|
19
|
+
this.status = status;
|
|
20
|
+
this.code = code;
|
|
21
|
+
this.details = details;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function errorHandler() {
|
|
26
|
+
return async (c: Context, next: Next) => {
|
|
27
|
+
try {
|
|
28
|
+
await next();
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err instanceof Error) {
|
|
31
|
+
if (!(err instanceof SourcePressError)) {
|
|
32
|
+
console.error("Unhandled error:", err);
|
|
33
|
+
}
|
|
34
|
+
return handleRouteError(err, c);
|
|
35
|
+
}
|
|
36
|
+
return c.json<ApiError>(
|
|
37
|
+
{ status: 500, message: "Internal server error", code: "INTERNAL_ERROR" },
|
|
38
|
+
500,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { corsMiddleware } from "./cors.js";
|
|
2
|
+
export { errorHandler, SourcePressError } from "./error-handler.js";
|
|
3
|
+
export type { ApiError } from "./error-handler.js";
|
|
4
|
+
export { authMiddleware, requireAuth } from "./auth.js";
|
|
5
|
+
export type { AuthContext } from "./auth.js";
|
|
6
|
+
export { handleRouteError } from "./route-error-handler.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { rateLimiter } from "hono-rate-limiter";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Rate limiter for AI-consuming endpoints (expensive operations).
|
|
5
|
+
* 10 requests per minute per IP.
|
|
6
|
+
*/
|
|
7
|
+
export function aiRateLimit() {
|
|
8
|
+
return rateLimiter({
|
|
9
|
+
windowMs: 60 * 1000,
|
|
10
|
+
limit: 10,
|
|
11
|
+
standardHeaders: "draft-6",
|
|
12
|
+
keyGenerator: (c) => c.req.header("x-forwarded-for") ?? c.req.header("x-real-ip") ?? "unknown",
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Rate limiter for general endpoints.
|
|
18
|
+
* 100 requests per minute per IP.
|
|
19
|
+
*/
|
|
20
|
+
export function generalRateLimit() {
|
|
21
|
+
return rateLimiter({
|
|
22
|
+
windowMs: 60 * 1000,
|
|
23
|
+
limit: 100,
|
|
24
|
+
standardHeaders: "draft-6",
|
|
25
|
+
keyGenerator: (c) => c.req.header("x-forwarded-for") ?? c.req.header("x-real-ip") ?? "unknown",
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Context } from "hono";
|
|
2
|
+
import { SourcePressError } from "./error-handler.js";
|
|
3
|
+
|
|
4
|
+
export function handleRouteError(err: Error, c: Context) {
|
|
5
|
+
if (err instanceof SourcePressError) {
|
|
6
|
+
return c.json(
|
|
7
|
+
{ status: err.status, message: err.message, code: err.code, details: err.details },
|
|
8
|
+
err.status as 400 | 404 | 500 | 501,
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
console.error("Unhandled route error:", err);
|
|
12
|
+
return c.json({ status: 500, message: "Internal server error", code: "INTERNAL_ERROR" }, 500);
|
|
13
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ContentChange } from "@sourcepress/core";
|
|
2
|
+
import { Hono } from "hono";
|
|
3
|
+
import type { EngineContext } from "../engine.js";
|
|
4
|
+
import { resolveApprovalRule } from "../engine.js";
|
|
5
|
+
import { SourcePressError } from "../middleware/error-handler.js";
|
|
6
|
+
import { handleRouteError } from "../middleware/route-error-handler.js";
|
|
7
|
+
|
|
8
|
+
export function approvalRoutes(engine: EngineContext) {
|
|
9
|
+
const app = new Hono();
|
|
10
|
+
|
|
11
|
+
// GET /approval/pending — list pending approval requests
|
|
12
|
+
app.get("/approval/pending", async (c) => {
|
|
13
|
+
if (!engine.approval) {
|
|
14
|
+
throw new SourcePressError(501, "APPROVAL_NOT_CONFIGURED", "Approval not configured");
|
|
15
|
+
}
|
|
16
|
+
const items = await engine.approval.pending();
|
|
17
|
+
return c.json({ items, total: items.length });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
// GET /approval/:id — get single approval request status
|
|
21
|
+
app.get("/approval/:id", async (c) => {
|
|
22
|
+
if (!engine.approval) {
|
|
23
|
+
throw new SourcePressError(501, "APPROVAL_NOT_CONFIGURED", "Approval not configured");
|
|
24
|
+
}
|
|
25
|
+
const id = c.req.param("id");
|
|
26
|
+
const status = await engine.approval.status(id);
|
|
27
|
+
return c.json({ id, status });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// POST /approval — submit content for approval (creates PR or commits directly)
|
|
31
|
+
app.post("/approval", async (c) => {
|
|
32
|
+
if (!engine.approval) {
|
|
33
|
+
throw new SourcePressError(501, "APPROVAL_NOT_CONFIGURED", "Approval not configured");
|
|
34
|
+
}
|
|
35
|
+
const body = await c.req.json<ContentChange>();
|
|
36
|
+
if (!body.collection || !body.slug || !body.path || !body.action || !body.content) {
|
|
37
|
+
throw new SourcePressError(
|
|
38
|
+
400,
|
|
39
|
+
"INVALID_INPUT",
|
|
40
|
+
"collection, slug, path, action, and content are required",
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const rule = resolveApprovalRule(engine.config, body.collection, body.path);
|
|
45
|
+
|
|
46
|
+
if (rule === "direct") {
|
|
47
|
+
await engine.github.createOrUpdateFile(
|
|
48
|
+
body.path,
|
|
49
|
+
body.content,
|
|
50
|
+
`chore: direct publish ${body.collection}/${body.slug}`,
|
|
51
|
+
);
|
|
52
|
+
return c.json({ status: "direct", path: body.path }, 201);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const request = await engine.approval.submit(body);
|
|
56
|
+
return c.json(request, 201);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// POST /approval/:id/approve — approve (merge PR)
|
|
60
|
+
app.post("/approval/:id/approve", async (c) => {
|
|
61
|
+
if (!engine.approval) {
|
|
62
|
+
throw new SourcePressError(501, "APPROVAL_NOT_CONFIGURED", "Approval not configured");
|
|
63
|
+
}
|
|
64
|
+
const id = c.req.param("id");
|
|
65
|
+
const body = await c.req.json<{ by: string; comment?: string }>();
|
|
66
|
+
if (!body.by) {
|
|
67
|
+
throw new SourcePressError(400, "INVALID_INPUT", "by is required");
|
|
68
|
+
}
|
|
69
|
+
await engine.approval.approve(id, body.by, body.comment);
|
|
70
|
+
return c.json({ status: "approved" });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// POST /approval/:id/reject — reject (close PR)
|
|
74
|
+
app.post("/approval/:id/reject", async (c) => {
|
|
75
|
+
if (!engine.approval) {
|
|
76
|
+
throw new SourcePressError(501, "APPROVAL_NOT_CONFIGURED", "Approval not configured");
|
|
77
|
+
}
|
|
78
|
+
const id = c.req.param("id");
|
|
79
|
+
const body = await c.req.json<{ by: string; reason: string }>();
|
|
80
|
+
if (!body.by || !body.reason) {
|
|
81
|
+
throw new SourcePressError(400, "INVALID_INPUT", "by and reason are required");
|
|
82
|
+
}
|
|
83
|
+
await engine.approval.reject(id, body.by, body.reason);
|
|
84
|
+
return c.json({ status: "rejected" });
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
app.onError(handleRouteError);
|
|
88
|
+
|
|
89
|
+
return app;
|
|
90
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { EngineContext } from "../engine.js";
|
|
3
|
+
import { SourcePressError } from "../middleware/error-handler.js";
|
|
4
|
+
import { handleRouteError } from "../middleware/route-error-handler.js";
|
|
5
|
+
|
|
6
|
+
function validateSlug(slug: string): void {
|
|
7
|
+
if (!/^[a-z0-9-]+$/.test(slug)) {
|
|
8
|
+
throw new SourcePressError(
|
|
9
|
+
400,
|
|
10
|
+
"INVALID_SLUG",
|
|
11
|
+
`Slug "${slug}" is invalid. Only lowercase letters, digits, and hyphens are allowed.`,
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function contentRoutes(engine: EngineContext) {
|
|
17
|
+
const app = new Hono();
|
|
18
|
+
|
|
19
|
+
app.onError(handleRouteError);
|
|
20
|
+
|
|
21
|
+
// GET /content — list all content across all collections
|
|
22
|
+
app.get("/content", async (c) => {
|
|
23
|
+
const collections = engine.listCollections();
|
|
24
|
+
const allContent: Array<{
|
|
25
|
+
collection: string;
|
|
26
|
+
slug: string;
|
|
27
|
+
path: string;
|
|
28
|
+
frontmatter: Record<string, unknown>;
|
|
29
|
+
}> = [];
|
|
30
|
+
|
|
31
|
+
for (const collection of collections) {
|
|
32
|
+
const files = await engine.listContent(collection);
|
|
33
|
+
for (const file of files) {
|
|
34
|
+
allContent.push({
|
|
35
|
+
collection: file.collection,
|
|
36
|
+
slug: file.slug,
|
|
37
|
+
path: file.path,
|
|
38
|
+
frontmatter: file.frontmatter,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return c.json({ items: allContent, total: allContent.length });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// GET /content/:collection — list content in a collection
|
|
47
|
+
app.get("/content/:collection", async (c) => {
|
|
48
|
+
const collection = c.req.param("collection");
|
|
49
|
+
const def = engine.getCollectionDef(collection);
|
|
50
|
+
|
|
51
|
+
if (!def) {
|
|
52
|
+
throw new SourcePressError(
|
|
53
|
+
404,
|
|
54
|
+
"COLLECTION_NOT_FOUND",
|
|
55
|
+
`Collection "${collection}" not found`,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const files = await engine.listContent(collection);
|
|
60
|
+
const items = files.map((f) => ({
|
|
61
|
+
collection: f.collection,
|
|
62
|
+
slug: f.slug,
|
|
63
|
+
path: f.path,
|
|
64
|
+
frontmatter: f.frontmatter,
|
|
65
|
+
body: f.body,
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
return c.json({ items, total: items.length, collection: def });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// GET /content/:collection/:slug — get a single content file
|
|
72
|
+
app.get("/content/:collection/:slug", async (c) => {
|
|
73
|
+
const collection = c.req.param("collection");
|
|
74
|
+
const slug = c.req.param("slug");
|
|
75
|
+
validateSlug(slug);
|
|
76
|
+
const def = engine.getCollectionDef(collection);
|
|
77
|
+
|
|
78
|
+
if (!def) {
|
|
79
|
+
throw new SourcePressError(
|
|
80
|
+
404,
|
|
81
|
+
"COLLECTION_NOT_FOUND",
|
|
82
|
+
`Collection "${collection}" not found`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const file = await engine.getContent(collection, slug);
|
|
87
|
+
if (!file) {
|
|
88
|
+
throw new SourcePressError(
|
|
89
|
+
404,
|
|
90
|
+
"CONTENT_NOT_FOUND",
|
|
91
|
+
`Content "${slug}" not found in "${collection}"`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return c.json(file);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// POST /content/:collection — create new content (returns PR)
|
|
99
|
+
app.post("/content/:collection", async (c) => {
|
|
100
|
+
const collection = c.req.param("collection");
|
|
101
|
+
const def = engine.getCollectionDef(collection);
|
|
102
|
+
|
|
103
|
+
if (!def) {
|
|
104
|
+
throw new SourcePressError(
|
|
105
|
+
404,
|
|
106
|
+
"COLLECTION_NOT_FOUND",
|
|
107
|
+
`Collection "${collection}" not found`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const body = await c.req.json<{
|
|
112
|
+
slug: string;
|
|
113
|
+
frontmatter: Record<string, unknown>;
|
|
114
|
+
body: string;
|
|
115
|
+
}>();
|
|
116
|
+
|
|
117
|
+
if (!body.slug || !body.body) {
|
|
118
|
+
throw new SourcePressError(400, "INVALID_INPUT", "slug and body are required");
|
|
119
|
+
}
|
|
120
|
+
validateSlug(body.slug);
|
|
121
|
+
|
|
122
|
+
// Build file content with frontmatter
|
|
123
|
+
const ext = def.format === "yaml" ? "yaml" : def.format === "json" ? "json" : def.format;
|
|
124
|
+
const basePath = def.path.replace(/\/+$/, "");
|
|
125
|
+
const filePath = `${basePath}/${body.slug}.${ext}`;
|
|
126
|
+
const frontmatterYaml = Object.entries(body.frontmatter ?? {})
|
|
127
|
+
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
|
|
128
|
+
.join("\n");
|
|
129
|
+
const fileContent = `---\n${frontmatterYaml}\n---\n\n${body.body}`;
|
|
130
|
+
|
|
131
|
+
// Create branch + commit + PR
|
|
132
|
+
const branchName = `sourcepress/create-${collection}-${body.slug}-${Date.now()}`;
|
|
133
|
+
await engine.github.createBranch(branchName);
|
|
134
|
+
await engine.github.createOrUpdateFile(
|
|
135
|
+
filePath,
|
|
136
|
+
fileContent,
|
|
137
|
+
`feat(${collection}): create ${body.slug}`,
|
|
138
|
+
branchName,
|
|
139
|
+
);
|
|
140
|
+
const pr = await engine.github.createPR(
|
|
141
|
+
`Create ${collection}: ${body.slug}`,
|
|
142
|
+
`Created by SourcePress API.\n\nCollection: ${collection}\nSlug: ${body.slug}`,
|
|
143
|
+
branchName,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
return c.json(
|
|
147
|
+
{ created: true, path: filePath, pr: { number: pr.number, url: pr.html_url } },
|
|
148
|
+
201,
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// PUT /content/:collection/:slug — update content (returns PR)
|
|
153
|
+
app.put("/content/:collection/:slug", async (c) => {
|
|
154
|
+
const collection = c.req.param("collection");
|
|
155
|
+
const slug = c.req.param("slug");
|
|
156
|
+
validateSlug(slug);
|
|
157
|
+
const def = engine.getCollectionDef(collection);
|
|
158
|
+
|
|
159
|
+
if (!def) {
|
|
160
|
+
throw new SourcePressError(
|
|
161
|
+
404,
|
|
162
|
+
"COLLECTION_NOT_FOUND",
|
|
163
|
+
`Collection "${collection}" not found`,
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const existing = await engine.getContent(collection, slug);
|
|
168
|
+
if (!existing) {
|
|
169
|
+
throw new SourcePressError(
|
|
170
|
+
404,
|
|
171
|
+
"CONTENT_NOT_FOUND",
|
|
172
|
+
`Content "${slug}" not found in "${collection}"`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const body = await c.req.json<{
|
|
177
|
+
frontmatter?: Record<string, unknown>;
|
|
178
|
+
body?: string;
|
|
179
|
+
}>();
|
|
180
|
+
|
|
181
|
+
const mergedFrontmatter = { ...existing.frontmatter, ...(body.frontmatter ?? {}) };
|
|
182
|
+
const mergedBody = body.body ?? existing.body;
|
|
183
|
+
|
|
184
|
+
const frontmatterYaml = Object.entries(mergedFrontmatter)
|
|
185
|
+
.map(([key, value]) => `${key}: ${JSON.stringify(value)}`)
|
|
186
|
+
.join("\n");
|
|
187
|
+
const fileContent = `---\n${frontmatterYaml}\n---\n\n${mergedBody}`;
|
|
188
|
+
|
|
189
|
+
// Get existing file SHA for update
|
|
190
|
+
const ext = def.format === "yaml" ? "yaml" : def.format === "json" ? "json" : def.format;
|
|
191
|
+
const filePath = `${def.path}/${slug}.${ext}`;
|
|
192
|
+
const existingFile = await engine.github.getFile(filePath);
|
|
193
|
+
const existingSha = existingFile?.sha;
|
|
194
|
+
|
|
195
|
+
const branchName = `sourcepress/update-${collection}-${slug}-${Date.now()}`;
|
|
196
|
+
await engine.github.createBranch(branchName);
|
|
197
|
+
await engine.github.createOrUpdateFile(
|
|
198
|
+
filePath,
|
|
199
|
+
fileContent,
|
|
200
|
+
`update(${collection}): update ${slug}`,
|
|
201
|
+
branchName,
|
|
202
|
+
existingSha,
|
|
203
|
+
);
|
|
204
|
+
const pr = await engine.github.createPR(
|
|
205
|
+
`Update ${collection}: ${slug}`,
|
|
206
|
+
`Updated by SourcePress API.\n\nCollection: ${collection}\nSlug: ${slug}`,
|
|
207
|
+
branchName,
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
return c.json({ updated: true, path: filePath, pr: { number: pr.number, url: pr.html_url } });
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// DELETE /content/:collection/:slug — delete content (returns PR)
|
|
214
|
+
app.delete("/content/:collection/:slug", async (c) => {
|
|
215
|
+
const collection = c.req.param("collection");
|
|
216
|
+
const slug = c.req.param("slug");
|
|
217
|
+
validateSlug(slug);
|
|
218
|
+
const def = engine.getCollectionDef(collection);
|
|
219
|
+
|
|
220
|
+
if (!def) {
|
|
221
|
+
throw new SourcePressError(
|
|
222
|
+
404,
|
|
223
|
+
"COLLECTION_NOT_FOUND",
|
|
224
|
+
`Collection "${collection}" not found`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const existing = await engine.getContent(collection, slug);
|
|
229
|
+
if (!existing) {
|
|
230
|
+
throw new SourcePressError(
|
|
231
|
+
404,
|
|
232
|
+
"CONTENT_NOT_FOUND",
|
|
233
|
+
`Content "${slug}" not found in "${collection}"`,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Delete is implemented as creating a PR that removes the file
|
|
238
|
+
// For now, we create a branch and commit an empty update with a delete marker
|
|
239
|
+
const branchName = `sourcepress/delete-${collection}-${slug}-${Date.now()}`;
|
|
240
|
+
await engine.github.createBranch(branchName);
|
|
241
|
+
|
|
242
|
+
const pr = await engine.github.createPR(
|
|
243
|
+
`Delete ${collection}: ${slug}`,
|
|
244
|
+
`Deletion requested by SourcePress API.\n\nCollection: ${collection}\nSlug: ${slug}\nPath: ${existing.path}`,
|
|
245
|
+
branchName,
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return c.json({
|
|
249
|
+
deleted: true,
|
|
250
|
+
path: existing.path,
|
|
251
|
+
pr: { number: pr.number, url: pr.html_url },
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return app;
|
|
256
|
+
}
|