contentbase 0.0.1 → 0.0.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.
- package/.mcp.json +9 -0
- package/CLI.md +593 -0
- package/MCP-DESCRIPTIONS-REVIEW.md +122 -0
- package/MCP-SERVER-SPEC.md +453 -0
- package/PRIMER.md +540 -0
- package/README.md +406 -13
- package/bun.lock +45 -6
- package/dist/cnotes +0 -0
- package/docs/README.md +110 -0
- package/docs/TABLE-OF-CONTENTS.md +7 -0
- package/docs/models.ts +38 -0
- package/models.ts +38 -0
- package/package.json +14 -4
- package/public/web-demo/index.html +813 -0
- package/scripts/examples/01-collection-setup.ts +46 -0
- package/scripts/examples/02-querying.ts +67 -0
- package/scripts/examples/03-sections.ts +36 -0
- package/scripts/examples/04-relationships.ts +54 -0
- package/scripts/examples/05-document-api.ts +54 -0
- package/scripts/examples/06-extract-sections.ts +55 -0
- package/scripts/examples/07-validation.ts +46 -0
- package/scripts/examples/08-serialization.ts +51 -0
- package/scripts/examples/lib/format.ts +87 -0
- package/scripts/examples/lib/setup.ts +21 -0
- package/scripts/examples/run-all.ts +43 -0
- package/showcases/node_modules/.cache/.repl_history +3 -0
- package/showcases/vinyl-collection/artists/nina-simone.mdx +1 -1
- package/src/__tests__/semantic-search.integration.test.ts +284 -0
- package/src/api/endpoints/actions.ts +34 -0
- package/src/api/endpoints/doc.ts +187 -0
- package/src/api/endpoints/docs-index.ts +26 -0
- package/src/api/endpoints/document.ts +171 -0
- package/src/api/endpoints/documents.ts +71 -0
- package/src/api/endpoints/inspect.ts +16 -0
- package/src/api/endpoints/models.ts +10 -0
- package/src/api/endpoints/query.ts +88 -0
- package/src/api/endpoints/root.ts +7 -0
- package/src/api/endpoints/search-reindex.ts +65 -0
- package/src/api/endpoints/search-status.ts +55 -0
- package/src/api/endpoints/search.ts +104 -0
- package/src/api/endpoints/semantic-search.ts +120 -0
- package/src/api/endpoints/text-search.ts +63 -0
- package/src/api/endpoints/validate.ts +34 -0
- package/src/api/helpers.ts +97 -0
- package/src/base-model.ts +12 -0
- package/src/cli/commands/action.ts +82 -44
- package/src/cli/commands/console.ts +124 -0
- package/src/cli/commands/create.ts +179 -53
- package/src/cli/commands/embed.ts +323 -0
- package/src/cli/commands/export.ts +58 -24
- package/src/cli/commands/extract.ts +174 -0
- package/src/cli/commands/help.ts +81 -0
- package/src/cli/commands/index.ts +17 -0
- package/src/cli/commands/init.ts +72 -48
- package/src/cli/commands/inspect.ts +56 -46
- package/src/cli/commands/mcp.ts +1219 -0
- package/src/cli/commands/search.ts +285 -0
- package/src/cli/commands/serve.ts +348 -0
- package/src/cli/commands/summary.ts +60 -0
- package/src/cli/commands/teach.ts +86 -0
- package/src/cli/commands/text-search.ts +134 -0
- package/src/cli/commands/validate.ts +126 -64
- package/src/cli/index.ts +88 -19
- package/src/cli/load-collection.ts +144 -17
- package/src/cli/registry.ts +28 -0
- package/src/collection.ts +455 -6
- package/src/define-model.ts +104 -7
- package/src/document.ts +47 -1
- package/src/extract-sections.ts +222 -0
- package/src/index.ts +20 -2
- package/src/model-instance.ts +35 -5
- package/src/node-shortcuts.ts +1 -1
- package/src/query/collection-query.ts +96 -9
- package/src/query/index.ts +7 -0
- package/src/query/query-dsl.ts +259 -0
- package/src/relationships/has-many.ts +39 -0
- package/src/relationships/index.ts +7 -2
- package/src/section.ts +2 -0
- package/src/types.ts +24 -2
- package/src/utils/index.ts +1 -0
- package/src/utils/match-pattern.ts +65 -0
- package/src/validator.ts +18 -1
- package/test/collection.test.ts +118 -2
- package/test/extract-sections.test.ts +356 -0
- package/test/fixtures/sdlc/MODELS.md +106 -0
- package/test/fixtures/sdlc/SKILL.md +404 -0
- package/test/fixtures/sdlc/index.ts +9 -0
- package/test/fixtures/sdlc/models.ts +8 -6
- package/test/fixtures/sdlc/stories/authentication/a-user-should-be-able-to-login.mdx +20 -0
- package/test/fixtures/sdlc/templates/epic.md +23 -0
- package/test/fixtures/sdlc/templates/story.md +19 -0
- package/test/pattern.test.ts +191 -0
- package/test/query-dsl.test.ts +431 -0
- package/test/query.test.ts +29 -0
- package/test/relationships.test.ts +130 -0
- package/test/section.test.ts +65 -4
- package/test/table-of-contents.test.ts +135 -0
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { unified } from 'unified'
|
|
2
|
+
import remarkParse from 'remark-parse'
|
|
3
|
+
import remarkGfm from 'remark-gfm'
|
|
4
|
+
import remarkRehype from 'remark-rehype'
|
|
5
|
+
import rehypeStringify from 'rehype-stringify'
|
|
6
|
+
import type { Collection } from '../collection.js'
|
|
7
|
+
import { introspectMetaSchema } from '../collection.js'
|
|
8
|
+
|
|
9
|
+
export async function renderMarkdownToHtml(markdown: string): Promise<string> {
|
|
10
|
+
const result = await unified()
|
|
11
|
+
.use(remarkParse)
|
|
12
|
+
.use(remarkGfm)
|
|
13
|
+
.use(remarkRehype)
|
|
14
|
+
.use(rehypeStringify)
|
|
15
|
+
.process(markdown)
|
|
16
|
+
return String(result)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function rewriteDocLinks(html: string): string {
|
|
20
|
+
return html.replace(/href="([^"]*\.(?:md|mdx))"/g, (match, href) => {
|
|
21
|
+
if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//')) {
|
|
22
|
+
return match
|
|
23
|
+
}
|
|
24
|
+
return `href="${href.replace(/\.mdx?$/, '')}"`
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const tocPageStyle = `<style>
|
|
29
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
30
|
+
body {
|
|
31
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
32
|
+
max-width: 52rem;
|
|
33
|
+
margin: 0 auto;
|
|
34
|
+
padding: 2rem 1.5rem;
|
|
35
|
+
line-height: 1.7;
|
|
36
|
+
color: #1a1a2e;
|
|
37
|
+
background: #fafafa;
|
|
38
|
+
}
|
|
39
|
+
h1 { font-size: 2rem; font-weight: 600; margin: 2rem 0 1rem; color: #0f0f23; }
|
|
40
|
+
h2 { font-size: 1.5rem; font-weight: 600; margin: 2.5rem 0 0.75rem; color: #16163a; border-bottom: 1px solid #e2e2e8; padding-bottom: 0.4rem; }
|
|
41
|
+
a { color: #2563eb; text-decoration: none; }
|
|
42
|
+
a:hover { text-decoration: underline; }
|
|
43
|
+
ul { padding-left: 1.5rem; margin: 0.5rem 0 1rem; }
|
|
44
|
+
li { margin: 0.3rem 0; }
|
|
45
|
+
</style>`
|
|
46
|
+
|
|
47
|
+
export async function renderTocPage(collection: Collection): Promise<string> {
|
|
48
|
+
const tocMarkdown = collection.tableOfContents({ title: 'Table of Contents', basePath: '.' })
|
|
49
|
+
const tocHtml = rewriteDocLinks(await renderMarkdownToHtml(tocMarkdown))
|
|
50
|
+
return `<!DOCTYPE html>
|
|
51
|
+
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">
|
|
52
|
+
<title>Table of Contents</title>
|
|
53
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
54
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
55
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap" rel="stylesheet">
|
|
56
|
+
${tocPageStyle}
|
|
57
|
+
</head>
|
|
58
|
+
<body>
|
|
59
|
+
${tocHtml}
|
|
60
|
+
</body></html>`
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function resolveModelDef(collection: Collection, name: string) {
|
|
64
|
+
const lower = name.toLowerCase()
|
|
65
|
+
return collection.modelDefinitions.find(
|
|
66
|
+
(d: any) => d.name.toLowerCase() === lower || d.prefix.toLowerCase() === lower,
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function buildSchemaJSON(collection: Collection) {
|
|
71
|
+
const models: Record<string, any> = {}
|
|
72
|
+
for (const def of collection.modelDefinitions as any[]) {
|
|
73
|
+
const fields = introspectMetaSchema(def.meta)
|
|
74
|
+
const sections = Object.entries(def.sections || {}).map(([key, sec]: [string, any]) => ({
|
|
75
|
+
key,
|
|
76
|
+
heading: sec.heading,
|
|
77
|
+
alternatives: sec.alternatives || [],
|
|
78
|
+
hasSchema: !!sec.schema,
|
|
79
|
+
}))
|
|
80
|
+
const relationships = Object.entries(def.relationships || {}).map(([key, rel]: [string, any]) => ({
|
|
81
|
+
key,
|
|
82
|
+
type: rel.type,
|
|
83
|
+
model: rel.model,
|
|
84
|
+
}))
|
|
85
|
+
models[def.name] = {
|
|
86
|
+
name: def.name,
|
|
87
|
+
prefix: def.prefix,
|
|
88
|
+
description: def.description,
|
|
89
|
+
fields,
|
|
90
|
+
sections,
|
|
91
|
+
relationships,
|
|
92
|
+
computed: Object.keys(def.computed || {}),
|
|
93
|
+
scopes: Object.keys(def.scopes || {}),
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return models
|
|
97
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { defineModel } from "./define-model";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Built-in Base model that serves as a catch-all for documents
|
|
6
|
+
* that don't match any other registered model.
|
|
7
|
+
* Auto-registered by Collection.load() unless overridden.
|
|
8
|
+
*/
|
|
9
|
+
export const Base = defineModel("Base", {
|
|
10
|
+
prefix: "",
|
|
11
|
+
meta: z.looseObject({}),
|
|
12
|
+
});
|
|
@@ -1,44 +1,82 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { commands } from '../registry.js'
|
|
3
|
+
import { loadCollection } from '../load-collection.js'
|
|
4
|
+
|
|
5
|
+
const argsSchema = z.object({
|
|
6
|
+
contentFolder: z.string().optional(),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
async function handler(options: z.infer<typeof argsSchema>, context: { container: any }) {
|
|
10
|
+
const collection = await loadCollection({
|
|
11
|
+
contentFolder: options.contentFolder,
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
const actionName = context.container.argv._[1] as string | undefined
|
|
15
|
+
|
|
16
|
+
if (!actionName) {
|
|
17
|
+
console.error('Usage: cnotes action <name>')
|
|
18
|
+
console.error(`Available: ${collection.availableActions.join(', ') || '(none)'}`)
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!collection.actions.has(actionName)) {
|
|
23
|
+
console.error(
|
|
24
|
+
`Action "${actionName}" not found. Available: ${collection.availableActions.join(', ') || '(none)'}`
|
|
25
|
+
)
|
|
26
|
+
process.exit(1)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const result = await collection.runAction(actionName)
|
|
30
|
+
if (result !== undefined) {
|
|
31
|
+
console.log(
|
|
32
|
+
typeof result === 'string'
|
|
33
|
+
? result
|
|
34
|
+
: JSON.stringify(result, null, 2)
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
commands.register('action', {
|
|
40
|
+
description: 'Run a named action on the collection',
|
|
41
|
+
help: `# cnotes action
|
|
42
|
+
|
|
43
|
+
Run a named action registered on the collection. Actions are custom functions defined in your collection's entry point.
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
\`\`\`
|
|
48
|
+
cnotes action <name> [options]
|
|
49
|
+
\`\`\`
|
|
50
|
+
|
|
51
|
+
## Arguments
|
|
52
|
+
|
|
53
|
+
| Argument | Description |
|
|
54
|
+
|----------|-------------|
|
|
55
|
+
| \`name\` | The registered action name |
|
|
56
|
+
|
|
57
|
+
## Options
|
|
58
|
+
|
|
59
|
+
| Option | Description |
|
|
60
|
+
|--------|-------------|
|
|
61
|
+
| \`--contentFolder\` | Path to content folder |
|
|
62
|
+
|
|
63
|
+
## Output
|
|
64
|
+
|
|
65
|
+
If the action returns a value, it is printed to stdout (strings directly, objects as JSON).
|
|
66
|
+
|
|
67
|
+
## Examples
|
|
68
|
+
|
|
69
|
+
\`\`\`bash
|
|
70
|
+
# Run an action
|
|
71
|
+
cnotes action generate-report
|
|
72
|
+
|
|
73
|
+
# Run an action on a specific content folder
|
|
74
|
+
cnotes action sync --contentFolder ./docs
|
|
75
|
+
|
|
76
|
+
# List available actions (shows error with available names)
|
|
77
|
+
cnotes action
|
|
78
|
+
\`\`\`
|
|
79
|
+
`,
|
|
80
|
+
argsSchema,
|
|
81
|
+
handler,
|
|
82
|
+
})
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { commands } from '../registry.js'
|
|
3
|
+
import { loadCollection } from '../load-collection.js'
|
|
4
|
+
|
|
5
|
+
const argsSchema = z.object({
|
|
6
|
+
contentFolder: z.string().optional(),
|
|
7
|
+
})
|
|
8
|
+
|
|
9
|
+
async function handler(options: z.infer<typeof argsSchema>, context: { container: any }) {
|
|
10
|
+
const container = context.container
|
|
11
|
+
const ui = container.feature('ui')
|
|
12
|
+
|
|
13
|
+
const collection = await loadCollection({
|
|
14
|
+
contentFolder: options.contentFolder,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
const featureContext: Record<string, any> = {
|
|
18
|
+
collection,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Expose all container features
|
|
22
|
+
for (const name of container.features.available) {
|
|
23
|
+
try {
|
|
24
|
+
featureContext[name] = container.feature(name)
|
|
25
|
+
} catch {}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Load user console module if present
|
|
29
|
+
const consoleModulePath = container.paths.resolve('cnotes.console.ts')
|
|
30
|
+
let consoleModuleLoaded = false
|
|
31
|
+
let consoleModuleError: Error | null = null
|
|
32
|
+
|
|
33
|
+
if (container.fs.exists(consoleModulePath)) {
|
|
34
|
+
try {
|
|
35
|
+
const vmFeature = container.feature('vm')
|
|
36
|
+
const userExports = vmFeature.loadModule(consoleModulePath, { container, console, collection })
|
|
37
|
+
Object.assign(featureContext, userExports)
|
|
38
|
+
consoleModuleLoaded = true
|
|
39
|
+
} catch (err: any) {
|
|
40
|
+
consoleModuleError = err
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const prompt = ui.colors.cyan('cnotes') + ui.colors.dim(' > ')
|
|
45
|
+
|
|
46
|
+
console.log()
|
|
47
|
+
console.log(ui.colors.dim(' Contentbase REPL — collection and container features in scope. Tab to autocomplete.'))
|
|
48
|
+
if (consoleModuleLoaded) {
|
|
49
|
+
console.log(ui.colors.dim(' Loaded cnotes.console.ts exports into scope.'))
|
|
50
|
+
} else if (consoleModuleError) {
|
|
51
|
+
console.log(ui.colors.yellow(' Warning: Failed to load cnotes.console.ts:'))
|
|
52
|
+
console.log(ui.colors.yellow(` ${consoleModuleError.message}`))
|
|
53
|
+
console.log(ui.colors.dim(' The REPL will start without your custom exports.'))
|
|
54
|
+
}
|
|
55
|
+
console.log(ui.colors.dim(' Type .exit to quit.'))
|
|
56
|
+
console.log()
|
|
57
|
+
|
|
58
|
+
const repl = container.feature('repl', { prompt })
|
|
59
|
+
await repl.start({
|
|
60
|
+
context: {
|
|
61
|
+
...featureContext,
|
|
62
|
+
console,
|
|
63
|
+
setTimeout,
|
|
64
|
+
setInterval,
|
|
65
|
+
clearTimeout,
|
|
66
|
+
clearInterval,
|
|
67
|
+
fetch,
|
|
68
|
+
Bun,
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
commands.register('console', {
|
|
74
|
+
description: 'Start an interactive REPL with collection and container features in scope',
|
|
75
|
+
help: `# cnotes console
|
|
76
|
+
|
|
77
|
+
Start an interactive REPL with the loaded collection, all container features, and optional user-defined exports in scope. Useful for exploring and debugging your content.
|
|
78
|
+
|
|
79
|
+
## Usage
|
|
80
|
+
|
|
81
|
+
\`\`\`
|
|
82
|
+
cnotes console [options]
|
|
83
|
+
\`\`\`
|
|
84
|
+
|
|
85
|
+
## Options
|
|
86
|
+
|
|
87
|
+
| Option | Description |
|
|
88
|
+
|--------|-------------|
|
|
89
|
+
| \`--contentFolder\` | Path to content folder |
|
|
90
|
+
|
|
91
|
+
## Scope
|
|
92
|
+
|
|
93
|
+
The REPL has these variables available:
|
|
94
|
+
|
|
95
|
+
- \`collection\` — The loaded Collection instance
|
|
96
|
+
- All luca container features (fs, ui, grep, etc.)
|
|
97
|
+
- Exports from \`cnotes.console.ts\` if present in the project root
|
|
98
|
+
|
|
99
|
+
## Custom Console Module
|
|
100
|
+
|
|
101
|
+
Create a \`cnotes.console.ts\` file in your project root to add custom helpers to the REPL scope:
|
|
102
|
+
|
|
103
|
+
\`\`\`typescript
|
|
104
|
+
export const myHelper = () => "hello from console"
|
|
105
|
+
\`\`\`
|
|
106
|
+
|
|
107
|
+
## Examples
|
|
108
|
+
|
|
109
|
+
\`\`\`bash
|
|
110
|
+
# Start the REPL
|
|
111
|
+
cnotes console
|
|
112
|
+
|
|
113
|
+
# Start with a specific content folder
|
|
114
|
+
cnotes console --contentFolder ./docs
|
|
115
|
+
|
|
116
|
+
# Once inside the REPL:
|
|
117
|
+
# collection.available — list all document IDs
|
|
118
|
+
# collection.document('posts/hello') — load a document
|
|
119
|
+
# .exit — quit
|
|
120
|
+
\`\`\`
|
|
121
|
+
`,
|
|
122
|
+
argsSchema,
|
|
123
|
+
handler,
|
|
124
|
+
})
|
|
@@ -1,59 +1,185 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import fs from
|
|
3
|
-
import path from
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import matter from 'gray-matter'
|
|
5
|
+
import { commands } from '../registry.js'
|
|
6
|
+
import { loadCollection } from '../load-collection.js'
|
|
7
|
+
import { kebabCase } from '../../utils/inflect.js'
|
|
8
|
+
import { introspectMetaSchema } from '../../collection.js'
|
|
9
|
+
|
|
10
|
+
const argsSchema = z.object({
|
|
11
|
+
title: z.string().optional(),
|
|
12
|
+
contentFolder: z.string().optional(),
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
async function handler(options: z.infer<typeof argsSchema>, context: { container: any }) {
|
|
16
|
+
const collection = await loadCollection({
|
|
17
|
+
contentFolder: options.contentFolder,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const modelName = context.container.argv._[1] as string | undefined
|
|
21
|
+
if (!modelName) {
|
|
22
|
+
console.error('Usage: cnotes create <model> --title "Document Title"')
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const title = options.title
|
|
27
|
+
if (!title) {
|
|
28
|
+
console.error('--title is required')
|
|
29
|
+
process.exit(1)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const def =
|
|
33
|
+
collection.getModelDefinition(modelName) ??
|
|
34
|
+
collection.modelDefinitions.find(
|
|
35
|
+
(d) => d.name.toLowerCase() === modelName.toLowerCase()
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
if (!def) {
|
|
39
|
+
console.error(
|
|
40
|
+
`Model "${modelName}" not found. Available: ${collection.modelDefinitions.map((d) => d.name).join(', ')}`
|
|
41
|
+
)
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Parse --meta.* flags from raw argv
|
|
46
|
+
const metaOverrides: Record<string, unknown> = {}
|
|
47
|
+
const rawArgs = context.container.argv
|
|
48
|
+
for (const key of Object.keys(rawArgs)) {
|
|
49
|
+
if (key.startsWith('meta.')) {
|
|
50
|
+
metaOverrides[key.slice(5)] = rawArgs[key]
|
|
43
51
|
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Build meta from priority layers: zod defaults < definition.defaults < template frontmatter < CLI overrides
|
|
55
|
+
const zodDefaults: Record<string, unknown> = {}
|
|
56
|
+
for (const field of introspectMetaSchema(def.meta)) {
|
|
57
|
+
if (field.defaultValue !== undefined) {
|
|
58
|
+
zodDefaults[field.name] = field.defaultValue
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const definitionDefaults: Record<string, unknown> = def.defaults ?? {}
|
|
44
63
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
64
|
+
// Template lookup
|
|
65
|
+
const templateExtensions = ['md', 'mdx']
|
|
66
|
+
let templateContent: string | null = null
|
|
67
|
+
|
|
68
|
+
for (const ext of templateExtensions) {
|
|
69
|
+
const templatePath = path.resolve(
|
|
48
70
|
collection.rootPath,
|
|
49
|
-
|
|
50
|
-
|
|
71
|
+
'templates',
|
|
72
|
+
`${modelName.toLowerCase()}.${ext}`
|
|
73
|
+
)
|
|
74
|
+
try {
|
|
75
|
+
templateContent = await fs.readFile(templatePath, 'utf8')
|
|
76
|
+
break
|
|
77
|
+
} catch {
|
|
78
|
+
// template not found, try next extension
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let content: string
|
|
83
|
+
|
|
84
|
+
if (templateContent) {
|
|
85
|
+
const parsed = matter(templateContent)
|
|
86
|
+
|
|
87
|
+
const mergedMeta = {
|
|
88
|
+
...zodDefaults,
|
|
89
|
+
...definitionDefaults,
|
|
90
|
+
...parsed.data,
|
|
91
|
+
...metaOverrides,
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const body = parsed.content.replace(/^# .+$/m, `# ${title}`)
|
|
95
|
+
content = matter.stringify(body, mergedMeta)
|
|
96
|
+
} else {
|
|
97
|
+
const mergedMeta = {
|
|
98
|
+
...zodDefaults,
|
|
99
|
+
...definitionDefaults,
|
|
100
|
+
...metaOverrides,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const lines: string[] = []
|
|
104
|
+
lines.push(`# ${title}`)
|
|
105
|
+
lines.push('')
|
|
106
|
+
|
|
107
|
+
const sections = def.sections ?? {}
|
|
108
|
+
for (const [, sec] of Object.entries(sections)) {
|
|
109
|
+
const s = sec as any
|
|
110
|
+
lines.push(`## ${s.heading}`)
|
|
111
|
+
lines.push('')
|
|
112
|
+
if (s.schema?.description) {
|
|
113
|
+
lines.push(s.schema.description)
|
|
114
|
+
lines.push('')
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
content = matter.stringify(lines.join('\n'), mergedMeta)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const slug = kebabCase(title.toLowerCase())
|
|
122
|
+
const pathId = `${def.prefix}/${slug}`
|
|
123
|
+
const filePath = path.resolve(collection.rootPath, `${pathId}.md`)
|
|
124
|
+
|
|
125
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
126
|
+
await fs.writeFile(filePath, content, 'utf8')
|
|
127
|
+
|
|
128
|
+
console.log(`Created ${filePath}`)
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
commands.register('create', {
|
|
132
|
+
description: 'Create a new document for a model type',
|
|
133
|
+
help: `# cnotes create
|
|
134
|
+
|
|
135
|
+
Create a new document for a registered model, with proper frontmatter defaults and section scaffolding.
|
|
136
|
+
|
|
137
|
+
## Usage
|
|
138
|
+
|
|
139
|
+
\`\`\`
|
|
140
|
+
cnotes create <model> --title "Document Title" [options]
|
|
141
|
+
\`\`\`
|
|
142
|
+
|
|
143
|
+
## Arguments
|
|
144
|
+
|
|
145
|
+
| Argument | Description |
|
|
146
|
+
|----------|-------------|
|
|
147
|
+
| \`model\` | Model name (e.g. \`Post\`, \`Epic\`, \`Task\`) |
|
|
148
|
+
|
|
149
|
+
## Options
|
|
150
|
+
|
|
151
|
+
| Option | Description |
|
|
152
|
+
|--------|-------------|
|
|
153
|
+
| \`--title\` | **Required.** Title for the new document (used as H1 and slug) |
|
|
154
|
+
| \`--meta.*\` | Set frontmatter fields (e.g. \`--meta.status active\`) |
|
|
155
|
+
| \`--contentFolder\` | Path to content folder |
|
|
156
|
+
|
|
157
|
+
## Template Lookup
|
|
158
|
+
|
|
159
|
+
If \`templates/<model>.md\` (or \`.mdx\`) exists in the content root, it will be used as the document scaffold. Template frontmatter is merged with schema defaults and CLI overrides.
|
|
160
|
+
|
|
161
|
+
## Meta Priority
|
|
162
|
+
|
|
163
|
+
Frontmatter values are merged in this order (last wins):
|
|
164
|
+
|
|
165
|
+
1. Zod schema defaults
|
|
166
|
+
2. Model definition defaults
|
|
167
|
+
3. Template frontmatter
|
|
168
|
+
4. CLI \`--meta.*\` overrides
|
|
169
|
+
|
|
170
|
+
## Examples
|
|
51
171
|
|
|
52
|
-
|
|
172
|
+
\`\`\`bash
|
|
173
|
+
# Create a new post
|
|
174
|
+
cnotes create Post --title "My First Post"
|
|
53
175
|
|
|
54
|
-
|
|
55
|
-
|
|
176
|
+
# Create with meta overrides
|
|
177
|
+
cnotes create Task --title "Fix login bug" --meta.status active --meta.priority high
|
|
56
178
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
179
|
+
# Create from a different content folder
|
|
180
|
+
cnotes create Epic --title "Auth System" --contentFolder ./docs
|
|
181
|
+
\`\`\`
|
|
182
|
+
`,
|
|
183
|
+
argsSchema,
|
|
184
|
+
handler,
|
|
185
|
+
})
|