@magek/mcp-server 0.0.8
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/README.md +42 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +25 -0
- package/dist/prompts/cqrs-flow.d.ts +15 -0
- package/dist/prompts/cqrs-flow.js +252 -0
- package/dist/prompts/troubleshooting.d.ts +15 -0
- package/dist/prompts/troubleshooting.js +239 -0
- package/dist/resources/cli-reference.d.ts +13 -0
- package/dist/resources/cli-reference.js +193 -0
- package/dist/resources/documentation.d.ts +18 -0
- package/dist/resources/documentation.js +62 -0
- package/dist/server.d.ts +5 -0
- package/dist/server.js +127 -0
- package/dist/utils/docs-loader.d.ts +19 -0
- package/dist/utils/docs-loader.js +111 -0
- package/docs/advanced/custom-templates.md +96 -0
- package/docs/advanced/data-migrations.md +181 -0
- package/docs/advanced/environment-configuration.md +74 -0
- package/docs/advanced/framework-packages.md +17 -0
- package/docs/advanced/health/sensor-health.md +389 -0
- package/docs/advanced/instrumentation.md +135 -0
- package/docs/advanced/register.md +119 -0
- package/docs/advanced/sensor.md +10 -0
- package/docs/advanced/testing.md +96 -0
- package/docs/advanced/touch-entities.md +45 -0
- package/docs/architecture/command.md +367 -0
- package/docs/architecture/entity.md +214 -0
- package/docs/architecture/event-driven.md +30 -0
- package/docs/architecture/event-handler.md +108 -0
- package/docs/architecture/event.md +145 -0
- package/docs/architecture/notifications.md +54 -0
- package/docs/architecture/queries.md +207 -0
- package/docs/architecture/read-model.md +507 -0
- package/docs/contributing.md +349 -0
- package/docs/docs-index.json +200 -0
- package/docs/features/error-handling.md +204 -0
- package/docs/features/event-stream.md +35 -0
- package/docs/features/logging.md +81 -0
- package/docs/features/schedule-actions.md +44 -0
- package/docs/getting-started/ai-coding-assistants.md +181 -0
- package/docs/getting-started/coding.md +543 -0
- package/docs/getting-started/installation.md +143 -0
- package/docs/graphql.md +1213 -0
- package/docs/index.md +62 -0
- package/docs/introduction.md +58 -0
- package/docs/magek-arch.png +0 -0
- package/docs/magek-cli.md +67 -0
- package/docs/magek-logo.svg +1 -0
- package/docs/security/authentication.md +189 -0
- package/docs/security/authorization.md +242 -0
- package/docs/security/security.md +16 -0
- package/package.json +46 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export const CLI_REFERENCE_URI = 'magek://cli/reference';
|
|
2
|
+
export const CLI_REFERENCE_CONTENT = `# Magek CLI Commands
|
|
3
|
+
|
|
4
|
+
Comprehensive reference for all Magek CLI scaffolding commands. These commands generate TypeScript files following Magek conventions.
|
|
5
|
+
|
|
6
|
+
## Scaffolding Commands
|
|
7
|
+
|
|
8
|
+
### Create a Command
|
|
9
|
+
|
|
10
|
+
Commands represent user intent that triggers events. They are the entry point for all write operations.
|
|
11
|
+
|
|
12
|
+
\`\`\`bash
|
|
13
|
+
npx magek new:command <Name> --fields field1:type1 field2:type2
|
|
14
|
+
\`\`\`
|
|
15
|
+
|
|
16
|
+
**Example:**
|
|
17
|
+
\`\`\`bash
|
|
18
|
+
npx magek new:command CreateProduct --fields sku:string price:number description:string
|
|
19
|
+
\`\`\`
|
|
20
|
+
|
|
21
|
+
**Available field types:** string, number, boolean, UUID, Date
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
### Create an Event
|
|
26
|
+
|
|
27
|
+
Events are immutable facts that happened in the system. They are the source of truth.
|
|
28
|
+
|
|
29
|
+
\`\`\`bash
|
|
30
|
+
npx magek new:event <Name> --fields field1:type1 field2:type2
|
|
31
|
+
\`\`\`
|
|
32
|
+
|
|
33
|
+
**Example:**
|
|
34
|
+
\`\`\`bash
|
|
35
|
+
npx magek new:event ProductCreated --fields productId:UUID sku:string price:number
|
|
36
|
+
\`\`\`
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
### Create an Entity
|
|
41
|
+
|
|
42
|
+
Entities (aggregate roots) maintain state from events. Use --reduces to specify which events the entity handles.
|
|
43
|
+
|
|
44
|
+
\`\`\`bash
|
|
45
|
+
npx magek new:entity <Name> --fields field1:type1 field2:type2 --reduces EventName
|
|
46
|
+
\`\`\`
|
|
47
|
+
|
|
48
|
+
**Example:**
|
|
49
|
+
\`\`\`bash
|
|
50
|
+
npx magek new:entity Product --fields sku:string price:number inStock:boolean --reduces ProductCreated
|
|
51
|
+
\`\`\`
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
### Create a Read Model
|
|
56
|
+
|
|
57
|
+
Read models are query-optimized projections. Use --projects to specify which entity to project.
|
|
58
|
+
|
|
59
|
+
\`\`\`bash
|
|
60
|
+
npx magek new:read-model <Name> --fields field1:type1 field2:type2 --projects Entity:joinKey
|
|
61
|
+
\`\`\`
|
|
62
|
+
|
|
63
|
+
**Example:**
|
|
64
|
+
\`\`\`bash
|
|
65
|
+
npx magek new:read-model ProductReadModel --fields sku:string price:number name:string --projects Product:id
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### Create an Event Handler
|
|
71
|
+
|
|
72
|
+
Event handlers react to events and perform side effects (send emails, call external APIs, etc.).
|
|
73
|
+
|
|
74
|
+
\`\`\`bash
|
|
75
|
+
npx magek new:event-handler <Name> --event EventName
|
|
76
|
+
\`\`\`
|
|
77
|
+
|
|
78
|
+
**Example:**
|
|
79
|
+
\`\`\`bash
|
|
80
|
+
npx magek new:event-handler SendWelcomeEmail --event UserRegistered
|
|
81
|
+
\`\`\`
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
### Create a Scheduled Command
|
|
86
|
+
|
|
87
|
+
Scheduled commands run on a cron schedule for recurring tasks.
|
|
88
|
+
|
|
89
|
+
\`\`\`bash
|
|
90
|
+
npx magek new:scheduled-command <Name>
|
|
91
|
+
\`\`\`
|
|
92
|
+
|
|
93
|
+
**Example:**
|
|
94
|
+
\`\`\`bash
|
|
95
|
+
npx magek new:scheduled-command CleanupExpiredSessions
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
### Create a Query
|
|
101
|
+
|
|
102
|
+
Queries retrieve data from read models without side effects.
|
|
103
|
+
|
|
104
|
+
\`\`\`bash
|
|
105
|
+
npx magek new:query <Name> --fields field1:type1
|
|
106
|
+
\`\`\`
|
|
107
|
+
|
|
108
|
+
**Example:**
|
|
109
|
+
\`\`\`bash
|
|
110
|
+
npx magek new:query GetProductBySku --fields sku:string
|
|
111
|
+
\`\`\`
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### Create a Type
|
|
116
|
+
|
|
117
|
+
Custom types for domain modeling.
|
|
118
|
+
|
|
119
|
+
\`\`\`bash
|
|
120
|
+
npx magek new:type <Name> --fields field1:type1 field2:type2
|
|
121
|
+
\`\`\`
|
|
122
|
+
|
|
123
|
+
**Example:**
|
|
124
|
+
\`\`\`bash
|
|
125
|
+
npx magek new:type Money --fields amount:number currency:string
|
|
126
|
+
\`\`\`
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Project Initialization
|
|
131
|
+
|
|
132
|
+
### Create a New Magek Project
|
|
133
|
+
|
|
134
|
+
\`\`\`bash
|
|
135
|
+
npx create-magek my-app
|
|
136
|
+
\`\`\`
|
|
137
|
+
|
|
138
|
+
Or with pnpm:
|
|
139
|
+
\`\`\`bash
|
|
140
|
+
pnpm create magek my-app
|
|
141
|
+
\`\`\`
|
|
142
|
+
|
|
143
|
+
---
|
|
144
|
+
|
|
145
|
+
## Development Server
|
|
146
|
+
|
|
147
|
+
### Start the Development Server
|
|
148
|
+
|
|
149
|
+
\`\`\`bash
|
|
150
|
+
npx magek start
|
|
151
|
+
\`\`\`
|
|
152
|
+
|
|
153
|
+
This starts:
|
|
154
|
+
- GraphQL API at http://localhost:3000/graphql
|
|
155
|
+
- GraphQL Playground for testing queries
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## Field Type Reference
|
|
160
|
+
|
|
161
|
+
| Type | Description | Example |
|
|
162
|
+
|------|-------------|---------|
|
|
163
|
+
| \`string\` | Text values | \`name:string\` |
|
|
164
|
+
| \`number\` | Numeric values | \`price:number\` |
|
|
165
|
+
| \`boolean\` | True/false values | \`isActive:boolean\` |
|
|
166
|
+
| \`UUID\` | Unique identifiers | \`productId:UUID\` |
|
|
167
|
+
| \`Date\` | Date/time values | \`createdAt:Date\` |
|
|
168
|
+
|
|
169
|
+
## Tips for Claude Code
|
|
170
|
+
|
|
171
|
+
When helping users scaffold Magek components:
|
|
172
|
+
|
|
173
|
+
1. **Start with the Command** - This defines the user action
|
|
174
|
+
2. **Create the Event** - This records what happened
|
|
175
|
+
3. **Create the Entity** - This maintains the state
|
|
176
|
+
4. **Create the Read Model** - This provides query access
|
|
177
|
+
|
|
178
|
+
Always use descriptive names that reflect the domain (e.g., \`CreateOrder\` not \`Create\`).
|
|
179
|
+
`;
|
|
180
|
+
export function getCliReferenceResource() {
|
|
181
|
+
return {
|
|
182
|
+
uri: CLI_REFERENCE_URI,
|
|
183
|
+
name: 'Magek CLI Reference',
|
|
184
|
+
description: 'Complete reference for Magek CLI scaffolding commands',
|
|
185
|
+
mimeType: 'text/markdown',
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
export function readCliReference() {
|
|
189
|
+
return {
|
|
190
|
+
content: CLI_REFERENCE_CONTENT,
|
|
191
|
+
mimeType: 'text/markdown',
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DocsLoader } from '../utils/docs-loader.js';
|
|
2
|
+
export interface DocumentationResource {
|
|
3
|
+
uri: string;
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class DocumentationResources {
|
|
9
|
+
private readonly docsLoader;
|
|
10
|
+
constructor(docsLoader: DocsLoader);
|
|
11
|
+
listResources(): Promise<DocumentationResource[]>;
|
|
12
|
+
readResource(uri: string): Promise<{
|
|
13
|
+
content: string;
|
|
14
|
+
mimeType: string;
|
|
15
|
+
}>;
|
|
16
|
+
private getIndex;
|
|
17
|
+
private organizeByCategory;
|
|
18
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export class DocumentationResources {
|
|
2
|
+
docsLoader;
|
|
3
|
+
constructor(docsLoader) {
|
|
4
|
+
this.docsLoader = docsLoader;
|
|
5
|
+
}
|
|
6
|
+
async listResources() {
|
|
7
|
+
const documents = await this.docsLoader.loadIndex();
|
|
8
|
+
const resources = documents.map((doc) => ({
|
|
9
|
+
uri: doc.uri,
|
|
10
|
+
name: doc.title,
|
|
11
|
+
description: doc.description || `Magek documentation: ${doc.path}`,
|
|
12
|
+
mimeType: 'text/markdown',
|
|
13
|
+
}));
|
|
14
|
+
// Add the index resource
|
|
15
|
+
resources.unshift({
|
|
16
|
+
uri: 'magek://docs/index',
|
|
17
|
+
name: 'Documentation Index',
|
|
18
|
+
description: 'Index of all Magek documentation topics',
|
|
19
|
+
mimeType: 'application/json',
|
|
20
|
+
});
|
|
21
|
+
return resources;
|
|
22
|
+
}
|
|
23
|
+
async readResource(uri) {
|
|
24
|
+
if (uri === 'magek://docs/index') {
|
|
25
|
+
return this.getIndex();
|
|
26
|
+
}
|
|
27
|
+
const path = this.docsLoader.resolveUriToPath(uri);
|
|
28
|
+
if (!path) {
|
|
29
|
+
throw new Error(`Invalid resource URI: ${uri}`);
|
|
30
|
+
}
|
|
31
|
+
const content = await this.docsLoader.loadDocument(path);
|
|
32
|
+
return { content, mimeType: 'text/markdown' };
|
|
33
|
+
}
|
|
34
|
+
async getIndex() {
|
|
35
|
+
const documents = await this.docsLoader.loadIndex();
|
|
36
|
+
const index = {
|
|
37
|
+
description: 'Magek Documentation Index',
|
|
38
|
+
topics: this.organizeByCategory(documents),
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
content: JSON.stringify(index, null, 2),
|
|
42
|
+
mimeType: 'application/json',
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
organizeByCategory(documents) {
|
|
46
|
+
const categories = {};
|
|
47
|
+
for (const doc of documents) {
|
|
48
|
+
// Extract category from path (e.g., "getting-started/installation.md" -> "getting-started")
|
|
49
|
+
const pathParts = doc.path.split('/');
|
|
50
|
+
const category = pathParts.length > 1 ? pathParts[0] : 'general';
|
|
51
|
+
if (!categories[category]) {
|
|
52
|
+
categories[category] = [];
|
|
53
|
+
}
|
|
54
|
+
categories[category].push({
|
|
55
|
+
title: doc.title,
|
|
56
|
+
uri: doc.uri,
|
|
57
|
+
description: doc.description,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return categories;
|
|
61
|
+
}
|
|
62
|
+
}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
3
|
+
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
|
|
4
|
+
import { DocsLoader } from './utils/docs-loader.js';
|
|
5
|
+
const require = createRequire(import.meta.url);
|
|
6
|
+
const packageJson = require('../package.json');
|
|
7
|
+
import { DocumentationResources } from './resources/documentation.js';
|
|
8
|
+
import { CLI_REFERENCE_URI, getCliReferenceResource, readCliReference, } from './resources/cli-reference.js';
|
|
9
|
+
import { CQRS_FLOW_PROMPT_NAME, getCqrsFlowPrompt, getCqrsFlowPromptDefinition, } from './prompts/cqrs-flow.js';
|
|
10
|
+
import { TROUBLESHOOTING_PROMPT_NAME, getTroubleshootingPrompt, getTroubleshootingPromptDefinition, } from './prompts/troubleshooting.js';
|
|
11
|
+
export function createMagekServer(options) {
|
|
12
|
+
const { docsPath } = options;
|
|
13
|
+
const docsLoader = new DocsLoader(docsPath);
|
|
14
|
+
const docResources = new DocumentationResources(docsLoader);
|
|
15
|
+
const server = new Server({
|
|
16
|
+
name: 'magek',
|
|
17
|
+
version: packageJson.version,
|
|
18
|
+
}, {
|
|
19
|
+
capabilities: {
|
|
20
|
+
resources: {},
|
|
21
|
+
prompts: {},
|
|
22
|
+
tools: {},
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
// List resources handler
|
|
26
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
27
|
+
const resources = await docResources.listResources();
|
|
28
|
+
// Add CLI reference resource
|
|
29
|
+
resources.push(getCliReferenceResource());
|
|
30
|
+
return {
|
|
31
|
+
resources: resources.map((r) => ({
|
|
32
|
+
uri: r.uri,
|
|
33
|
+
name: r.name,
|
|
34
|
+
description: r.description,
|
|
35
|
+
mimeType: r.mimeType,
|
|
36
|
+
})),
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
// Read resource handler
|
|
40
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
|
|
41
|
+
const { uri } = request.params;
|
|
42
|
+
// Handle CLI reference
|
|
43
|
+
if (uri === CLI_REFERENCE_URI) {
|
|
44
|
+
const { content, mimeType } = readCliReference();
|
|
45
|
+
return {
|
|
46
|
+
contents: [
|
|
47
|
+
{
|
|
48
|
+
uri,
|
|
49
|
+
mimeType,
|
|
50
|
+
text: content,
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Handle documentation resources
|
|
56
|
+
if (uri.startsWith('magek://docs/')) {
|
|
57
|
+
const { content, mimeType } = await docResources.readResource(uri);
|
|
58
|
+
return {
|
|
59
|
+
contents: [
|
|
60
|
+
{
|
|
61
|
+
uri,
|
|
62
|
+
mimeType,
|
|
63
|
+
text: content,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
throw new Error(`Unknown resource: ${uri}`);
|
|
69
|
+
});
|
|
70
|
+
// List prompts handler
|
|
71
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
72
|
+
return {
|
|
73
|
+
prompts: [getCqrsFlowPromptDefinition(), getTroubleshootingPromptDefinition()],
|
|
74
|
+
};
|
|
75
|
+
});
|
|
76
|
+
// Get prompt handler
|
|
77
|
+
server.setRequestHandler(GetPromptRequestSchema, async (request) => {
|
|
78
|
+
const { name, arguments: args } = request.params;
|
|
79
|
+
if (name === CQRS_FLOW_PROMPT_NAME) {
|
|
80
|
+
const feature = args?.feature;
|
|
81
|
+
if (!feature || typeof feature !== 'string') {
|
|
82
|
+
throw new Error('Required argument "feature" is missing');
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
description: `CQRS implementation guide for: ${feature}`,
|
|
86
|
+
messages: [
|
|
87
|
+
{
|
|
88
|
+
role: 'user',
|
|
89
|
+
content: {
|
|
90
|
+
type: 'text',
|
|
91
|
+
text: getCqrsFlowPrompt({ feature }),
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
],
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (name === TROUBLESHOOTING_PROMPT_NAME) {
|
|
98
|
+
const issue = args?.issue;
|
|
99
|
+
return {
|
|
100
|
+
description: issue
|
|
101
|
+
? `Troubleshooting guide for: ${issue}`
|
|
102
|
+
: 'General Magek troubleshooting guide',
|
|
103
|
+
messages: [
|
|
104
|
+
{
|
|
105
|
+
role: 'user',
|
|
106
|
+
content: {
|
|
107
|
+
type: 'text',
|
|
108
|
+
text: getTroubleshootingPrompt({
|
|
109
|
+
issue: typeof issue === 'string' ? issue : undefined,
|
|
110
|
+
}),
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
throw new Error(`Unknown prompt: ${name}`);
|
|
117
|
+
});
|
|
118
|
+
// List tools handler - no tools for now, just resources and prompts
|
|
119
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
120
|
+
return { tools: [] };
|
|
121
|
+
});
|
|
122
|
+
// Call tool handler - no tools for now
|
|
123
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
124
|
+
throw new Error(`Unknown tool: ${request.params.name}`);
|
|
125
|
+
});
|
|
126
|
+
return server;
|
|
127
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface DocumentInfo {
|
|
2
|
+
uri: string;
|
|
3
|
+
path: string;
|
|
4
|
+
title: string;
|
|
5
|
+
description: string | null;
|
|
6
|
+
}
|
|
7
|
+
export interface DocsIndex {
|
|
8
|
+
documents: DocumentInfo[];
|
|
9
|
+
}
|
|
10
|
+
export declare class DocsLoader {
|
|
11
|
+
private readonly docsPath;
|
|
12
|
+
constructor(docsPath: string);
|
|
13
|
+
loadDocument(relativePath: string): Promise<string>;
|
|
14
|
+
loadIndex(): Promise<DocumentInfo[]>;
|
|
15
|
+
private scanDocuments;
|
|
16
|
+
private extractTitle;
|
|
17
|
+
private extractDescription;
|
|
18
|
+
resolveUriToPath(uri: string): string | null;
|
|
19
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from 'node:fs/promises';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { join, normalize, relative, resolve } from 'node:path';
|
|
4
|
+
export class DocsLoader {
|
|
5
|
+
docsPath;
|
|
6
|
+
constructor(docsPath) {
|
|
7
|
+
this.docsPath = docsPath;
|
|
8
|
+
}
|
|
9
|
+
async loadDocument(relativePath) {
|
|
10
|
+
const fullPath = resolve(this.docsPath, relativePath);
|
|
11
|
+
const normalizedDocsPath = resolve(this.docsPath);
|
|
12
|
+
// Security: ensure the resolved path is within docsPath
|
|
13
|
+
if (!fullPath.startsWith(normalizedDocsPath + '/') && fullPath !== normalizedDocsPath) {
|
|
14
|
+
throw new Error(`Invalid path: ${relativePath}`);
|
|
15
|
+
}
|
|
16
|
+
if (!existsSync(fullPath)) {
|
|
17
|
+
throw new Error(`Document not found: ${relativePath}`);
|
|
18
|
+
}
|
|
19
|
+
return readFile(fullPath, 'utf-8');
|
|
20
|
+
}
|
|
21
|
+
async loadIndex() {
|
|
22
|
+
const indexPath = join(this.docsPath, 'docs-index.json');
|
|
23
|
+
if (existsSync(indexPath)) {
|
|
24
|
+
const content = await readFile(indexPath, 'utf-8');
|
|
25
|
+
return JSON.parse(content);
|
|
26
|
+
}
|
|
27
|
+
// Fallback: scan directory
|
|
28
|
+
return this.scanDocuments();
|
|
29
|
+
}
|
|
30
|
+
async scanDocuments(dir = this.docsPath, documents = []) {
|
|
31
|
+
if (!existsSync(dir)) {
|
|
32
|
+
return documents;
|
|
33
|
+
}
|
|
34
|
+
const entries = await readdir(dir);
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
const fullPath = join(dir, entry);
|
|
37
|
+
const stats = await stat(fullPath);
|
|
38
|
+
if (stats.isDirectory()) {
|
|
39
|
+
await this.scanDocuments(fullPath, documents);
|
|
40
|
+
}
|
|
41
|
+
else if (entry.endsWith('.md')) {
|
|
42
|
+
const relativePath = relative(this.docsPath, fullPath);
|
|
43
|
+
const uri = relativePath.replace(/\.md$/, '').replace(/\\/g, '/');
|
|
44
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
45
|
+
documents.push({
|
|
46
|
+
uri: `magek://docs/${uri}`,
|
|
47
|
+
path: relativePath.replace(/\\/g, '/'),
|
|
48
|
+
title: this.extractTitle(content) || uri.split('/').pop() || uri,
|
|
49
|
+
description: this.extractDescription(content),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return documents;
|
|
54
|
+
}
|
|
55
|
+
extractTitle(content) {
|
|
56
|
+
// Try to extract title from first H1 heading
|
|
57
|
+
const h1Match = content.match(/^#\s+(.+)$/m);
|
|
58
|
+
if (h1Match) {
|
|
59
|
+
return h1Match[1].trim();
|
|
60
|
+
}
|
|
61
|
+
// Try to extract from frontmatter title
|
|
62
|
+
const frontmatterMatch = content.match(/^---[\s\S]*?title:\s*['"]?([^'"\n]+)['"]?[\s\S]*?---/m);
|
|
63
|
+
if (frontmatterMatch) {
|
|
64
|
+
return frontmatterMatch[1].trim();
|
|
65
|
+
}
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
extractDescription(content) {
|
|
69
|
+
// Remove frontmatter
|
|
70
|
+
const contentWithoutFrontmatter = content.replace(/^---[\s\S]*?---\n*/m, '');
|
|
71
|
+
// Find first paragraph after H1
|
|
72
|
+
const lines = contentWithoutFrontmatter.split('\n');
|
|
73
|
+
let foundH1 = false;
|
|
74
|
+
let description = '';
|
|
75
|
+
for (const line of lines) {
|
|
76
|
+
if (line.startsWith('# ')) {
|
|
77
|
+
foundH1 = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (foundH1 && line.trim() && !line.startsWith('#')) {
|
|
81
|
+
description = line.trim();
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
// Truncate to reasonable length
|
|
86
|
+
if (description.length > 200) {
|
|
87
|
+
description = description.substring(0, 197) + '...';
|
|
88
|
+
}
|
|
89
|
+
return description || null;
|
|
90
|
+
}
|
|
91
|
+
resolveUriToPath(uri) {
|
|
92
|
+
// Convert magek://docs/getting-started/installation -> getting-started/installation.md
|
|
93
|
+
const match = uri.match(/^magek:\/\/docs\/(.+)$/);
|
|
94
|
+
if (!match) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const pathSegment = match[1];
|
|
98
|
+
// Security: reject path traversal attempts and absolute paths
|
|
99
|
+
if (pathSegment.includes('..') ||
|
|
100
|
+
pathSegment.startsWith('/') ||
|
|
101
|
+
pathSegment.includes('\\')) {
|
|
102
|
+
return null;
|
|
103
|
+
}
|
|
104
|
+
// Normalize and verify it's a clean relative path
|
|
105
|
+
const normalized = normalize(pathSegment);
|
|
106
|
+
if (normalized.startsWith('..') || normalized.startsWith('/')) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
return `${normalized}.md`;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: "Custom Templates"
|
|
3
|
+
group: "Advanced"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Customizing CLI resource templates
|
|
7
|
+
|
|
8
|
+
You can change what the newly created Magek resources will contain by customizing the resource template files.
|
|
9
|
+
|
|
10
|
+
To do this, you first need to publish the resource templates by running the `npx magek stub:publish` command. This will create a folder `stubs` in the root directory of the project, and it will contain all the resources that you can customize:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
stubs/
|
|
14
|
+
├─ command.stub
|
|
15
|
+
├─ entity.stub
|
|
16
|
+
├─ event.stub
|
|
17
|
+
├─ event-handler.stub
|
|
18
|
+
├─ read-model.stub
|
|
19
|
+
├─ scheduled-command.stub
|
|
20
|
+
└─ type.stub
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
After that, Magek CLI will start using your local templates instead of the default ones.
|
|
24
|
+
Let's try this by adding a simple comment to the `type.stub` file.
|
|
25
|
+
|
|
26
|
+
```
|
|
27
|
+
// Look I am a comment that will now appear in every new type file 🐙
|
|
28
|
+
export class {{{ name }}} {
|
|
29
|
+
public constructor(
|
|
30
|
+
{{#fields}}
|
|
31
|
+
public {{{name}}}: {{{type}}},
|
|
32
|
+
{{/fields}}
|
|
33
|
+
) {}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Now if you run `npx magek new:type CartItem --fields sku:string` command, you will get `common/cart-item.ts` file with following content:
|
|
38
|
+
```typescript
|
|
39
|
+
// Look I am a comment that will now appear in every new type file 🐙
|
|
40
|
+
export class CartItem {
|
|
41
|
+
public constructor(
|
|
42
|
+
public sku: string,
|
|
43
|
+
) {}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
You did it, we just updated our resource template file! Now when you run `npx magek new:type`, it will contain the comment you added earlier 🚀
|
|
48
|
+
Of course, this is a simple example, and you may want to add new methods, import something, you name it!
|
|
49
|
+
|
|
50
|
+
Here are some answers to questions you may have:
|
|
51
|
+
|
|
52
|
+
#### QA
|
|
53
|
+
|
|
54
|
+
**Can I have only one stub for a certain resource?**
|
|
55
|
+
|
|
56
|
+
Yes! The resource generator will check if you have a custom template or it will use the default template
|
|
57
|
+
|
|
58
|
+
**How can I keep up with new template updates?**
|
|
59
|
+
|
|
60
|
+
1. Run `npx magek stub:publish --force` command
|
|
61
|
+
2. Review changes
|
|
62
|
+
3. Done!
|
|
63
|
+
|
|
64
|
+
**Can I adjust the command template and leave the other resources as they are?**
|
|
65
|
+
|
|
66
|
+
Yes. You can only have the `command.stub` file in the `/stubs` folder and customize it.
|
|
67
|
+
The generator will use the default templates for the other resources.
|
|
68
|
+
|
|
69
|
+
**How can I use the default templates again!?**
|
|
70
|
+
|
|
71
|
+
Simply delete the `/stubs` folder or a specific resource file.
|
|
72
|
+
|
|
73
|
+
**What are these strange name, #fields, etc. things????**
|
|
74
|
+
|
|
75
|
+
These are the variables and sections used by the mustache.js templating engine.
|
|
76
|
+
They allow us to dynamically generate new resources.
|
|
77
|
+
|
|
78
|
+
**How do I use custom project templates?**
|
|
79
|
+
|
|
80
|
+
Project creation now uses the modern `npm create` pattern instead of the CLI. You can use custom project templates in two ways:
|
|
81
|
+
|
|
82
|
+
**From GitHub repositories:**
|
|
83
|
+
```bash
|
|
84
|
+
npm create magek@latest my-app -- --template user/custom-template
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**From local filesystem:**
|
|
88
|
+
```bash
|
|
89
|
+
npm create magek@latest my-app -- --template ./path/to/local/template
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The template should contain a Magek project structure with mustache placeholders like `{{PROJECT_NAME}}`, `{{description}}`, etc.
|
|
93
|
+
|
|
94
|
+
**I have another question!**
|
|
95
|
+
|
|
96
|
+
You can ask questions on our Discord channel or create discussion on Github.
|