create-m5kdev 0.38.22 → 0.40.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/package.json +1 -1
- package/templates/minimal-app/apps/e2e/tests/files.spec.ts.tpl +4 -0
- package/templates/minimal-app/apps/server/AGENTS.md.tpl +2 -5
- package/templates/minimal-app/apps/server/src/modules/posts/posts.module.ts.tpl +0 -11
- package/templates/minimal-app/apps/server/src/modules/posts/posts.trpc.ts.tpl +18 -0
- package/templates/minimal-app/apps/webapp/src/modules/files/FilesRoute.tsx.tpl +24 -0
- package/templates/minimal-app/apps/webapp/src/modules/files/hooks/useFilesRoute.ts.tpl +33 -5
- package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +7 -3
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +12 -12
- package/templates/minimal-app/template.manifest.json +0 -1
- package/templates/minimal-app/apps/server/src/modules/posts/posts.mcp.ts.tpl +0 -17
package/package.json
CHANGED
|
@@ -32,4 +32,8 @@ test("upload a file and see it inventoried", async ({ page, request }) => {
|
|
|
32
32
|
await expect(page.getByTestId("file-row").first()).toContainText("hello.png", {
|
|
33
33
|
timeout: 15_000,
|
|
34
34
|
});
|
|
35
|
+
await expect(page.getByTestId("file-download")).toHaveAttribute(
|
|
36
|
+
"href",
|
|
37
|
+
/\/files\/[0-9a-f-]{36}$/i
|
|
38
|
+
);
|
|
35
39
|
});
|
|
@@ -20,9 +20,6 @@ apps/server/src/modules/<module>/
|
|
|
20
20
|
├── <module>.repository.ts
|
|
21
21
|
├── <module>.service.ts
|
|
22
22
|
├── <module>.trpc.ts
|
|
23
|
-
// m5k:mcp:start
|
|
24
|
-
├── <module>.mcp.ts
|
|
25
|
-
// m5k:mcp:end
|
|
26
23
|
└── <module>.module.ts
|
|
27
24
|
```
|
|
28
25
|
|
|
@@ -32,7 +29,7 @@ apps/server/src/modules/<module>/
|
|
|
32
29
|
- Services own business rules, orchestration, and context-aware defaults.
|
|
33
30
|
- tRPC files own transport only and must delegate to services.
|
|
34
31
|
// m5k:mcp:start
|
|
35
|
-
-
|
|
32
|
+
- Opt a tRPC procedure into the MCP catalog with `.meta({ mcp: { name, description } })`. The MCP tool is the procedure; Grant checks always run. Write instruction-style descriptions for MCP clients.
|
|
36
33
|
// m5k:mcp:end
|
|
37
34
|
- Register Core Modules and app modules in `apps/server/src/app.ts` via `createBackendApp(config, [modules])`. Do not use `backendApp.use`. Optional Backend Modules (`@m5kdev/module-*`) are not scaffolded; add them only when the product needs Clay, Docx, Pdf, Social, or Video.
|
|
38
35
|
// m5k:ai:start
|
|
@@ -57,7 +54,7 @@ apps/server/src/modules/<module>/
|
|
|
57
54
|
- After changing Drizzle tables, run `pnpm --filter ./apps/server drizzle:generate` then `drizzle:migrate` — do not hand-edit SQL migrations in this repo.
|
|
58
55
|
|
|
59
56
|
// m5k:mcp:start
|
|
60
|
-
- `McpModule` is registered in `app.ts`. MCP clients authenticate with Better Auth MCP OAuth (not API keys). Compose `auth.oauth.db` plus `mcpAllowlistEntries`.
|
|
57
|
+
- `McpModule` is registered in `app.ts`. MCP clients authenticate with Better Auth MCP OAuth (not API keys). Compose `auth.oauth.db` plus `mcpAllowlistEntries`. Posts exposes `list-posts` / `create-post` through `meta.mcp` on its tRPC procedures. Omit the module to disable MCP.
|
|
61
58
|
// m5k:mcp:end
|
|
62
59
|
|
|
63
60
|
// m5k:notifications:start
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
// biome-ignore-all assist/source/organizeImports: feature-gated posts.mcp import stays in marker blocks
|
|
2
1
|
import { createBackendRouterMap } from "@m5kdev/backend/app";
|
|
3
2
|
import {
|
|
4
3
|
BaseModule,
|
|
@@ -11,10 +10,6 @@ import { postsGrants } from "./posts.grants";
|
|
|
11
10
|
import { PostsRepository } from "./posts.repository";
|
|
12
11
|
import { PostsService } from "./posts.service";
|
|
13
12
|
import { createPostsTRPC } from "./posts.trpc";
|
|
14
|
-
// m5k:mcp:start
|
|
15
|
-
import type { ModuleMcpContext } from "@m5kdev/backend/modules/base/base.module";
|
|
16
|
-
import { createPostsMcp } from "./posts.mcp";
|
|
17
|
-
// m5k:mcp:end
|
|
18
13
|
|
|
19
14
|
type PostsModuleDeps = Record<string, never>;
|
|
20
15
|
type PostsModuleTables = typeof postsTables;
|
|
@@ -59,10 +54,4 @@ export class PostsModule extends BaseModule<
|
|
|
59
54
|
override trpc({ trpc, services }: ModuleTRPCContext<PostsModuleDeps, PostsModuleServices>) {
|
|
60
55
|
return createBackendRouterMap("posts", createPostsTRPC(trpc, services.posts));
|
|
61
56
|
}
|
|
62
|
-
|
|
63
|
-
// m5k:mcp:start
|
|
64
|
-
override mcp({ services }: ModuleMcpContext<PostsModuleDeps, PostsModuleServices>) {
|
|
65
|
-
return createPostsMcp(services.posts);
|
|
66
|
-
}
|
|
67
|
-
// m5k:mcp:end
|
|
68
57
|
}
|
|
@@ -8,11 +8,29 @@ export function createPostsTRPC(
|
|
|
8
8
|
) {
|
|
9
9
|
return router({
|
|
10
10
|
list: procedure
|
|
11
|
+
// m5k:mcp:start
|
|
12
|
+
.meta({
|
|
13
|
+
mcp: {
|
|
14
|
+
name: "list-posts",
|
|
15
|
+
description:
|
|
16
|
+
"List posts in an Organization. Call list-organizations first to learn organizationId values, then pass one as organizationId.",
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
// m5k:mcp:end
|
|
11
20
|
.input(postSchemas.input.list)
|
|
12
21
|
.output(postSchemas.output.list)
|
|
13
22
|
.query(async ({ ctx, input }) => handleTRPCResult(await postsService.list(input ?? {}, ctx))),
|
|
14
23
|
|
|
15
24
|
create: procedure
|
|
25
|
+
// m5k:mcp:start
|
|
26
|
+
.meta({
|
|
27
|
+
mcp: {
|
|
28
|
+
name: "create-post",
|
|
29
|
+
description:
|
|
30
|
+
"Create a draft post in an Organization. Call list-organizations first to learn organizationId values, then pass one as organizationId.",
|
|
31
|
+
},
|
|
32
|
+
})
|
|
33
|
+
// m5k:mcp:end
|
|
16
34
|
.input(postSchemas.input.create)
|
|
17
35
|
.output(postSchemas.output.single)
|
|
18
36
|
.mutation(async ({ ctx, input }) => handleTRPCResult(await postsService.create(input, ctx))),
|
|
@@ -79,6 +79,30 @@ export function FilesRoute() {
|
|
|
79
79
|
</Chip>
|
|
80
80
|
) : null}
|
|
81
81
|
</Card.Header>
|
|
82
|
+
<Card.Content className="flex flex-wrap items-center gap-3">
|
|
83
|
+
<img
|
|
84
|
+
src={row.downloadUrl}
|
|
85
|
+
alt={row.originalName}
|
|
86
|
+
className="h-16 w-16 rounded-md object-cover"
|
|
87
|
+
data-testid="file-preview"
|
|
88
|
+
/>
|
|
89
|
+
<a
|
|
90
|
+
href={row.downloadUrl}
|
|
91
|
+
data-testid="file-download"
|
|
92
|
+
className="text-sm font-medium underline"
|
|
93
|
+
>
|
|
94
|
+
{t("files.row.open")}
|
|
95
|
+
</a>
|
|
96
|
+
<Button
|
|
97
|
+
size="sm"
|
|
98
|
+
variant="ghost"
|
|
99
|
+
data-testid="file-delete"
|
|
100
|
+
isDisabled={files.isDeleting}
|
|
101
|
+
onPress={() => files.remove(row.id)}
|
|
102
|
+
>
|
|
103
|
+
{t("files.row.delete")}
|
|
104
|
+
</Button>
|
|
105
|
+
</Card.Content>
|
|
82
106
|
</Card>
|
|
83
107
|
))}
|
|
84
108
|
</div>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useAppConfig } from "@m5kdev/frontend/modules/app/hooks/useAppConfig";
|
|
2
|
+
import { fileCookieDownloadUrl } from "@m5kdev/frontend/modules/file/hooks/useS3DownloadUrl";
|
|
3
|
+
import { useS3Upload } from "@m5kdev/frontend/modules/file/hooks/useS3Upload";
|
|
2
4
|
import type { AppRouter } from "{{PACKAGE_SCOPE}}/server/types";
|
|
3
5
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
6
|
import type { inferRouterOutputs } from "@trpc/server";
|
|
@@ -9,25 +11,32 @@ import { useTRPC } from "@/utils/trpc";
|
|
|
9
11
|
type FileListOutput = inferRouterOutputs<AppRouter>["file"]["list"];
|
|
10
12
|
type FileRow = FileListOutput["rows"][number];
|
|
11
13
|
|
|
14
|
+
interface FilesRouteRow extends FileRow {
|
|
15
|
+
readonly downloadUrl: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
interface FilesRouteState {
|
|
13
|
-
readonly rows:
|
|
19
|
+
readonly rows: FilesRouteRow[];
|
|
14
20
|
readonly isLoading: boolean;
|
|
15
21
|
readonly isFetching: boolean;
|
|
16
22
|
readonly isUploading: boolean;
|
|
23
|
+
readonly isDeleting: boolean;
|
|
17
24
|
readonly upload: (file: File) => void;
|
|
25
|
+
readonly remove: (fileId: string) => void;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export function useFilesRoute(): FilesRouteState {
|
|
21
29
|
const { t } = useTranslation("{{APP_SLUG}}");
|
|
30
|
+
const { serverUrl } = useAppConfig();
|
|
22
31
|
const trpc = useTRPC();
|
|
23
32
|
const queryClient = useQueryClient();
|
|
24
|
-
const fileUpload =
|
|
33
|
+
const fileUpload = useS3Upload({ scope: "organization" });
|
|
25
34
|
|
|
26
35
|
const listQuery = useQuery(trpc.file.list.queryOptions({}));
|
|
27
36
|
|
|
28
37
|
const uploadMutation = useMutation({
|
|
29
38
|
mutationFn: async (file: File) => {
|
|
30
|
-
await fileUpload.upload(
|
|
39
|
+
await fileUpload.upload(file);
|
|
31
40
|
},
|
|
32
41
|
onSuccess: async () => {
|
|
33
42
|
toast.success(t("files.toast.uploaded"));
|
|
@@ -38,13 +47,32 @@ export function useFilesRoute(): FilesRouteState {
|
|
|
38
47
|
},
|
|
39
48
|
});
|
|
40
49
|
|
|
50
|
+
const deleteMutation = useMutation(
|
|
51
|
+
trpc.file.delete.mutationOptions({
|
|
52
|
+
onSuccess: async () => {
|
|
53
|
+
toast.success(t("files.toast.deleted"));
|
|
54
|
+
await queryClient.invalidateQueries(trpc.file.list.queryFilter());
|
|
55
|
+
},
|
|
56
|
+
onError: (error) => {
|
|
57
|
+
toast.error(error instanceof Error ? error.message : t("files.toast.deleteFailed"));
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
|
|
41
62
|
return {
|
|
42
|
-
rows: listQuery.data?.rows ?? []
|
|
63
|
+
rows: (listQuery.data?.rows ?? []).map((row) => ({
|
|
64
|
+
...row,
|
|
65
|
+
downloadUrl: fileCookieDownloadUrl(serverUrl, row.id),
|
|
66
|
+
})),
|
|
43
67
|
isLoading: listQuery.isLoading,
|
|
44
68
|
isFetching: listQuery.isFetching,
|
|
45
69
|
isUploading: uploadMutation.isPending,
|
|
70
|
+
isDeleting: deleteMutation.isPending,
|
|
46
71
|
upload: (file: File) => {
|
|
47
72
|
void uploadMutation.mutateAsync(file).catch(() => undefined);
|
|
48
73
|
},
|
|
74
|
+
remove: (fileId: string) => {
|
|
75
|
+
void deleteMutation.mutateAsync({ fileId }).catch(() => undefined);
|
|
76
|
+
},
|
|
49
77
|
};
|
|
50
78
|
}
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"hero": {
|
|
169
169
|
"eyebrow": "Files",
|
|
170
170
|
"title": "Upload an image and see it inventoried.",
|
|
171
|
-
"body": "
|
|
171
|
+
"body": "The browser PUTs to S3 after initiate, then finalize inventories the File. Open uses /files/:id.",
|
|
172
172
|
"upload": "Upload image",
|
|
173
173
|
"syncing": "Refreshing",
|
|
174
174
|
"synced": "Live against the files table"
|
|
@@ -178,11 +178,15 @@
|
|
|
178
178
|
"body": "Upload an image to see it listed here."
|
|
179
179
|
},
|
|
180
180
|
"row": {
|
|
181
|
-
"organization": "Organization"
|
|
181
|
+
"organization": "Organization",
|
|
182
|
+
"open": "Open",
|
|
183
|
+
"delete": "Delete"
|
|
182
184
|
},
|
|
183
185
|
"toast": {
|
|
184
186
|
"uploaded": "File uploaded.",
|
|
185
|
-
"failed": "Upload failed."
|
|
187
|
+
"failed": "Upload failed.",
|
|
188
|
+
"deleted": "File deleted.",
|
|
189
|
+
"deleteFailed": "Delete failed."
|
|
186
190
|
}
|
|
187
191
|
},
|
|
188
192
|
"notifications": {
|
|
@@ -7,18 +7,18 @@ catalogs:
|
|
|
7
7
|
"@heroui/react": 3.0.3
|
|
8
8
|
"@heroui/styles": 3.0.3
|
|
9
9
|
"@libsql/client": 0.17.4
|
|
10
|
-
"@m5kdev/backend": 0.
|
|
11
|
-
"@m5kdev/commons": 0.
|
|
12
|
-
"@m5kdev/config": 0.
|
|
13
|
-
"@m5kdev/email": 0.
|
|
14
|
-
"@m5kdev/expo": 0.
|
|
15
|
-
"@m5kdev/frontend": 0.
|
|
16
|
-
"@m5kdev/module-clay": 0.
|
|
17
|
-
"@m5kdev/module-docx": 0.
|
|
18
|
-
"@m5kdev/module-pdf": 0.
|
|
19
|
-
"@m5kdev/module-social": 0.
|
|
20
|
-
"@m5kdev/module-video": 0.
|
|
21
|
-
"@m5kdev/web-ui": 0.
|
|
10
|
+
"@m5kdev/backend": 0.40.0
|
|
11
|
+
"@m5kdev/commons": 0.40.0
|
|
12
|
+
"@m5kdev/config": 0.40.0
|
|
13
|
+
"@m5kdev/email": 0.40.0
|
|
14
|
+
"@m5kdev/expo": 0.40.0
|
|
15
|
+
"@m5kdev/frontend": 0.40.0
|
|
16
|
+
"@m5kdev/module-clay": 0.40.0
|
|
17
|
+
"@m5kdev/module-docx": 0.40.0
|
|
18
|
+
"@m5kdev/module-pdf": 0.40.0
|
|
19
|
+
"@m5kdev/module-social": 0.40.0
|
|
20
|
+
"@m5kdev/module-video": 0.40.0
|
|
21
|
+
"@m5kdev/web-ui": 0.40.0
|
|
22
22
|
"@mastra/core": 1.51.0
|
|
23
23
|
"@openrouter/ai-sdk-provider": 3.0.0
|
|
24
24
|
"@opentelemetry/api": 1.9.0
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { defineMcpCall } from "@m5kdev/backend/modules/mcp/mcp.define";
|
|
2
|
-
import type { McpOrganizationCallDefinition } from "@m5kdev/backend/modules/mcp/mcp.types";
|
|
3
|
-
import { postSchemas } from "./posts.dto";
|
|
4
|
-
import type { PostsService } from "./posts.service";
|
|
5
|
-
|
|
6
|
-
export function createPostsMcp(posts: PostsService): Record<string, McpOrganizationCallDefinition> {
|
|
7
|
-
return {
|
|
8
|
-
"list-posts": defineMcpCall()
|
|
9
|
-
.description("List posts in the selected Organization")
|
|
10
|
-
.input(postSchemas.input.list)
|
|
11
|
-
.handle(async (input, actor) => posts.list(input, { actor, user: { id: actor.userId } })),
|
|
12
|
-
"create-post": defineMcpCall()
|
|
13
|
-
.description("Create a draft post in the selected Organization")
|
|
14
|
-
.input(postSchemas.input.create)
|
|
15
|
-
.handle(async (input, actor) => posts.create(input, { actor, user: { id: actor.userId } })),
|
|
16
|
-
};
|
|
17
|
-
}
|