contentbase 0.0.2 → 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 +289 -13
- package/bun.lock +43 -4
- 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 +12 -3
- package/public/web-demo/index.html +813 -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 +361 -10
- package/src/define-model.ts +101 -4
- package/src/document.ts +47 -1
- package/src/extract-sections.ts +1 -1
- package/src/index.ts +14 -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 +23 -1
- 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/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 +61 -0
- package/test/table-of-contents.test.ts +49 -5
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import fs from 'fs/promises'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { commands } from '../registry.js'
|
|
5
|
+
import { loadCollection } from '../load-collection.js'
|
|
6
|
+
|
|
7
|
+
const argsSchema = z.object({
|
|
8
|
+
contentFolder: z.string().optional(),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
async function handler(options: z.infer<typeof argsSchema>) {
|
|
12
|
+
const collection = await loadCollection({
|
|
13
|
+
contentFolder: options.contentFolder,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const modelsSummary = collection.generateModelSummary()
|
|
17
|
+
const toc = collection.tableOfContents({ title: 'Table of Contents' })
|
|
18
|
+
|
|
19
|
+
// Read the bundled static docs from the contentbase package
|
|
20
|
+
const packageRoot = path.resolve(import.meta.dir, '../../..')
|
|
21
|
+
const primer = await fs.readFile(path.join(packageRoot, 'PRIMER.md'), 'utf8')
|
|
22
|
+
const cli = await fs.readFile(path.join(packageRoot, 'CLI.md'), 'utf8')
|
|
23
|
+
|
|
24
|
+
const output = [
|
|
25
|
+
modelsSummary.trimEnd(),
|
|
26
|
+
'',
|
|
27
|
+
'---',
|
|
28
|
+
'',
|
|
29
|
+
toc.trimEnd(),
|
|
30
|
+
'',
|
|
31
|
+
'---',
|
|
32
|
+
'',
|
|
33
|
+
cli.trimEnd(),
|
|
34
|
+
'',
|
|
35
|
+
'---',
|
|
36
|
+
'',
|
|
37
|
+
primer.trimEnd(),
|
|
38
|
+
'',
|
|
39
|
+
].join('\n')
|
|
40
|
+
|
|
41
|
+
console.log(output)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
commands.register('teach', {
|
|
45
|
+
description: 'Output combined documentation (README.md + TOC + CLI.md + PRIMER.md) for LLM context',
|
|
46
|
+
help: `# cnotes teach
|
|
47
|
+
|
|
48
|
+
Output combined documentation suitable for feeding to an LLM as context. Concatenates the models summary, table of contents, CLI reference, and API primer into a single stream.
|
|
49
|
+
|
|
50
|
+
## Usage
|
|
51
|
+
|
|
52
|
+
\`\`\`
|
|
53
|
+
cnotes teach [options]
|
|
54
|
+
\`\`\`
|
|
55
|
+
|
|
56
|
+
## Options
|
|
57
|
+
|
|
58
|
+
| Option | Description |
|
|
59
|
+
|--------|-------------|
|
|
60
|
+
| \`--contentFolder\` | Path to content folder |
|
|
61
|
+
|
|
62
|
+
## Output
|
|
63
|
+
|
|
64
|
+
Writes to stdout in this order:
|
|
65
|
+
|
|
66
|
+
1. **README.md** — Model definitions summary
|
|
67
|
+
2. **TABLE-OF-CONTENTS.md** — Document listing
|
|
68
|
+
3. **CLI.md** — CLI reference
|
|
69
|
+
4. **PRIMER.md** — API primer
|
|
70
|
+
|
|
71
|
+
## Examples
|
|
72
|
+
|
|
73
|
+
\`\`\`bash
|
|
74
|
+
# Print full documentation
|
|
75
|
+
cnotes teach
|
|
76
|
+
|
|
77
|
+
# Save to a file for LLM context
|
|
78
|
+
cnotes teach > context.md
|
|
79
|
+
|
|
80
|
+
# Teach from a specific content folder
|
|
81
|
+
cnotes teach --contentFolder ./docs
|
|
82
|
+
\`\`\`
|
|
83
|
+
`,
|
|
84
|
+
argsSchema,
|
|
85
|
+
handler,
|
|
86
|
+
})
|
|
@@ -0,0 +1,134 @@
|
|
|
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
|
+
include: z.string().optional(),
|
|
7
|
+
exclude: z.string().optional(),
|
|
8
|
+
ignoreCase: z.boolean().default(false),
|
|
9
|
+
expanded: z.boolean().default(false),
|
|
10
|
+
maxResults: z.number().optional(),
|
|
11
|
+
contentFolder: z.string().optional(),
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
async function handler(options: z.infer<typeof argsSchema>, { container }: { container: any }) {
|
|
15
|
+
const pattern = container.argv._[1] as string | undefined
|
|
16
|
+
|
|
17
|
+
if (!pattern) {
|
|
18
|
+
console.error('Usage: cnotes text-search <pattern> [options]')
|
|
19
|
+
console.error(' --expanded Show line-level matches (default: files only)')
|
|
20
|
+
console.error(' --include Glob filter (e.g. "*.md")')
|
|
21
|
+
console.error(' --exclude Glob filter')
|
|
22
|
+
console.error(' --ignoreCase Case insensitive search')
|
|
23
|
+
console.error(' --maxResults Limit number of results')
|
|
24
|
+
process.exit(1)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const collection = await loadCollection({
|
|
28
|
+
contentFolder: options.contentFolder,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const grep = container.feature('grep')
|
|
32
|
+
const searchPath = collection.rootPath
|
|
33
|
+
|
|
34
|
+
const grepOptions: any = {
|
|
35
|
+
pattern,
|
|
36
|
+
path: searchPath,
|
|
37
|
+
ignoreCase: options.ignoreCase,
|
|
38
|
+
maxResults: options.maxResults,
|
|
39
|
+
include: options.include,
|
|
40
|
+
exclude: options.exclude,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (!options.expanded) {
|
|
44
|
+
const files = await grep.filesContaining(pattern, {
|
|
45
|
+
path: searchPath,
|
|
46
|
+
ignoreCase: options.ignoreCase,
|
|
47
|
+
maxResults: options.maxResults,
|
|
48
|
+
include: options.include,
|
|
49
|
+
exclude: options.exclude,
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
if (files.length === 0) {
|
|
53
|
+
console.log('No matches found.')
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
console.log(`${files.length} file(s) match:\n`)
|
|
58
|
+
for (const file of files) {
|
|
59
|
+
console.log(` ${file}`)
|
|
60
|
+
}
|
|
61
|
+
} else {
|
|
62
|
+
const results = await grep.search(grepOptions)
|
|
63
|
+
|
|
64
|
+
if (results.length === 0) {
|
|
65
|
+
console.log('No matches found.')
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Group by file
|
|
70
|
+
const grouped = new Map<string, typeof results>()
|
|
71
|
+
for (const match of results) {
|
|
72
|
+
if (!grouped.has(match.file)) grouped.set(match.file, [])
|
|
73
|
+
grouped.get(match.file)!.push(match)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`${grouped.size} file(s), ${results.length} match(es):\n`)
|
|
77
|
+
for (const [file, matches] of grouped) {
|
|
78
|
+
console.log(` ${file}`)
|
|
79
|
+
for (const m of matches) {
|
|
80
|
+
console.log(` ${m.line}: ${m.content}`)
|
|
81
|
+
}
|
|
82
|
+
console.log()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
commands.register('text-search', {
|
|
88
|
+
description: 'Search file contents with pattern matching',
|
|
89
|
+
help: `# cnotes text-search
|
|
90
|
+
|
|
91
|
+
Search file contents within the collection using ripgrep. Returns matching files by default, or line-level detail with \`--expanded\`.
|
|
92
|
+
|
|
93
|
+
## Usage
|
|
94
|
+
|
|
95
|
+
\`\`\`
|
|
96
|
+
cnotes text-search <pattern> [options]
|
|
97
|
+
\`\`\`
|
|
98
|
+
|
|
99
|
+
## Arguments
|
|
100
|
+
|
|
101
|
+
| Argument | Description |
|
|
102
|
+
|----------|-------------|
|
|
103
|
+
| \`pattern\` | Text or regex pattern to search for |
|
|
104
|
+
|
|
105
|
+
## Options
|
|
106
|
+
|
|
107
|
+
| Option | Default | Description |
|
|
108
|
+
|--------|---------|-------------|
|
|
109
|
+
| \`--expanded\` | \`false\` | Show line-level matches instead of just file paths |
|
|
110
|
+
| \`--include\` | | Glob filter for file types (e.g. \`"*.md"\`) |
|
|
111
|
+
| \`--exclude\` | | Glob filter to exclude (e.g. \`"node_modules"\`) |
|
|
112
|
+
| \`--ignoreCase\` | \`false\` | Case-insensitive matching |
|
|
113
|
+
| \`--maxResults\` | | Limit number of results |
|
|
114
|
+
| \`--contentFolder\` | | Path to content folder |
|
|
115
|
+
|
|
116
|
+
## Examples
|
|
117
|
+
|
|
118
|
+
\`\`\`bash
|
|
119
|
+
# Find files containing "authentication"
|
|
120
|
+
cnotes text-search authentication
|
|
121
|
+
|
|
122
|
+
# Case-insensitive search with line details
|
|
123
|
+
cnotes text-search "TODO" --ignoreCase --expanded
|
|
124
|
+
|
|
125
|
+
# Search only markdown files
|
|
126
|
+
cnotes text-search "status: draft" --include "*.md"
|
|
127
|
+
|
|
128
|
+
# Limit results
|
|
129
|
+
cnotes text-search "import" --maxResults 10 --expanded
|
|
130
|
+
\`\`\`
|
|
131
|
+
`,
|
|
132
|
+
argsSchema,
|
|
133
|
+
handler,
|
|
134
|
+
})
|
|
@@ -1,75 +1,137 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
-
if (target === "all") {
|
|
31
|
-
pathIds = collection.available;
|
|
32
|
-
} else if (collection.items.has(target)) {
|
|
33
|
-
pathIds = [target];
|
|
1
|
+
import { z } from 'zod'
|
|
2
|
+
import { commands } from '../registry.js'
|
|
3
|
+
import { loadCollection } from '../load-collection.js'
|
|
4
|
+
import { validateDocument } from '../../validator.js'
|
|
5
|
+
|
|
6
|
+
const argsSchema = z.object({
|
|
7
|
+
contentFolder: z.string().optional(),
|
|
8
|
+
setDefaultMeta: z.boolean().default(false),
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
async function handler(options: z.infer<typeof argsSchema>, context: { container: any }) {
|
|
12
|
+
const collection = await loadCollection({
|
|
13
|
+
contentFolder: options.contentFolder,
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
const setDefaultMeta = options.setDefaultMeta
|
|
17
|
+
const target = (context.container.argv._[1] as string) || 'all'
|
|
18
|
+
let pathIds: string[]
|
|
19
|
+
|
|
20
|
+
if (target === 'all') {
|
|
21
|
+
pathIds = collection.available
|
|
22
|
+
} else if (collection.items.has(target)) {
|
|
23
|
+
pathIds = [target]
|
|
24
|
+
} else {
|
|
25
|
+
const def = collection.getModelDefinition(target)
|
|
26
|
+
if (def) {
|
|
27
|
+
pathIds = collection.available.filter((id) =>
|
|
28
|
+
id.startsWith(def.prefix)
|
|
29
|
+
)
|
|
34
30
|
} else {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (def) {
|
|
38
|
-
pathIds = collection.available.filter((id) =>
|
|
39
|
-
id.startsWith(def.prefix)
|
|
40
|
-
);
|
|
41
|
-
} else {
|
|
42
|
-
console.error(`Not found: "${target}"`);
|
|
43
|
-
process.exit(1);
|
|
44
|
-
}
|
|
31
|
+
console.error(`Not found: "${target}"`)
|
|
32
|
+
process.exit(1)
|
|
45
33
|
}
|
|
34
|
+
}
|
|
46
35
|
|
|
47
|
-
|
|
48
|
-
|
|
36
|
+
let valid = 0
|
|
37
|
+
let invalid = 0
|
|
38
|
+
let updated = 0
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
40
|
+
for (const pathId of pathIds) {
|
|
41
|
+
const def = collection.findModelDefinition(pathId)
|
|
42
|
+
if (!def) continue
|
|
53
43
|
|
|
54
|
-
|
|
55
|
-
const result = validateDocument(doc, def);
|
|
44
|
+
const doc = collection.document(pathId)
|
|
56
45
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
46
|
+
if (setDefaultMeta && Object.keys(doc.meta).length === 0) {
|
|
47
|
+
const defaults = def.meta.parse({})
|
|
48
|
+
if (Object.keys(defaults).length > 0) {
|
|
49
|
+
const cleanDefaults: Record<string, unknown> = {}
|
|
50
|
+
for (const [k, v] of Object.entries(defaults)) {
|
|
51
|
+
if (v !== undefined) cleanDefaults[k] = v
|
|
52
|
+
}
|
|
53
|
+
if (Object.keys(cleanDefaults).length > 0) {
|
|
54
|
+
Object.assign(doc.meta, cleanDefaults)
|
|
55
|
+
await doc.save({ normalize: false })
|
|
56
|
+
updated++
|
|
57
|
+
console.log(`SET DEFAULTS: ${pathId}`)
|
|
64
58
|
}
|
|
65
59
|
}
|
|
66
60
|
}
|
|
67
61
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
62
|
+
const result = validateDocument(doc, def)
|
|
63
|
+
|
|
64
|
+
if (result.valid) {
|
|
65
|
+
valid++
|
|
66
|
+
} else {
|
|
67
|
+
invalid++
|
|
68
|
+
console.log(`INVALID: ${pathId}`)
|
|
69
|
+
for (const error of result.errors) {
|
|
70
|
+
console.log(` ${error.path.join('.')}: ${error.message}`)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
console.log()
|
|
76
|
+
if (updated > 0) {
|
|
77
|
+
console.log(`Updated ${updated} document(s) with default meta.`)
|
|
78
|
+
}
|
|
79
|
+
console.log(
|
|
80
|
+
`Validated ${valid + invalid} documents: ${valid} valid, ${invalid} invalid`
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
if (invalid > 0) process.exit(1)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
commands.register('validate', {
|
|
87
|
+
description: 'Validate documents against their model schemas',
|
|
88
|
+
help: `# cnotes validate
|
|
89
|
+
|
|
90
|
+
Validate documents against their model schemas. Check frontmatter types, required fields, and optionally fill in missing defaults.
|
|
91
|
+
|
|
92
|
+
## Usage
|
|
93
|
+
|
|
94
|
+
\`\`\`
|
|
95
|
+
cnotes validate [target] [options]
|
|
96
|
+
\`\`\`
|
|
97
|
+
|
|
98
|
+
## Arguments
|
|
99
|
+
|
|
100
|
+
| Argument | Description | Default |
|
|
101
|
+
|----------|-------------|---------|
|
|
102
|
+
| \`target\` | A path ID, model name, or \`all\` | \`all\` |
|
|
103
|
+
|
|
104
|
+
## Options
|
|
105
|
+
|
|
106
|
+
| Option | Description |
|
|
107
|
+
|--------|-------------|
|
|
108
|
+
| \`--setDefaultMeta\` | Write Zod schema defaults to documents with empty frontmatter |
|
|
109
|
+
| \`--contentFolder\` | Path to content folder |
|
|
110
|
+
|
|
111
|
+
## Exit Codes
|
|
112
|
+
|
|
113
|
+
- \`0\` — All documents valid
|
|
114
|
+
- \`1\` — One or more validation errors
|
|
115
|
+
|
|
116
|
+
## Examples
|
|
117
|
+
|
|
118
|
+
\`\`\`bash
|
|
119
|
+
# Validate everything
|
|
120
|
+
cnotes validate
|
|
121
|
+
|
|
122
|
+
# Validate a single document
|
|
123
|
+
cnotes validate epics/auth-system
|
|
124
|
+
|
|
125
|
+
# Validate all documents of a model
|
|
126
|
+
cnotes validate Epic
|
|
127
|
+
|
|
128
|
+
# Fill in missing defaults and validate
|
|
129
|
+
cnotes validate all --setDefaultMeta
|
|
72
130
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
131
|
+
# Validate a different content folder
|
|
132
|
+
cnotes validate --contentFolder ./docs
|
|
133
|
+
\`\`\`
|
|
134
|
+
`,
|
|
135
|
+
argsSchema,
|
|
136
|
+
handler,
|
|
137
|
+
})
|
package/src/cli/index.ts
CHANGED
|
@@ -1,20 +1,89 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
2
|
+
import { commands } from './registry.js'
|
|
3
|
+
|
|
4
|
+
// Side-effect imports register all commands
|
|
5
|
+
import './commands/index.js'
|
|
6
|
+
|
|
7
|
+
async function renderMarkdown(container: any, text: string) {
|
|
8
|
+
const ui = container.feature('ui')
|
|
9
|
+
console.log(ui.markdown(text))
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function main() {
|
|
13
|
+
// Dynamic import so the library stays luca-free; only the CLI pulls it in
|
|
14
|
+
const luca = await import('@soederpop/luca/node')
|
|
15
|
+
const container = luca.default
|
|
16
|
+
|
|
17
|
+
const commandName = container.argv._[0] as string | undefined
|
|
18
|
+
const wantsHelp = container.argv.help || container.argv.h
|
|
19
|
+
|
|
20
|
+
// Bare invocation or explicit "help" with no subcommand
|
|
21
|
+
if (!commandName || (commandName === 'help' && !container.argv._[1])) {
|
|
22
|
+
const help = commands.get('help')!
|
|
23
|
+
await help.handler({}, { container })
|
|
24
|
+
return
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// `cnotes help <command>` — show that command's help
|
|
28
|
+
if (commandName === 'help' && container.argv._[1]) {
|
|
29
|
+
const target = container.argv._[1] as string
|
|
30
|
+
if (!commands.has(target)) {
|
|
31
|
+
console.error(`Unknown command: ${target}`)
|
|
32
|
+
console.error(`Run "cnotes help" to see available commands.\n`)
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
const def = commands.get(target)!
|
|
36
|
+
if (def.help) {
|
|
37
|
+
await renderMarkdown(container, def.help)
|
|
38
|
+
} else {
|
|
39
|
+
console.log(`${target} — ${def.description}\n`)
|
|
40
|
+
console.log(`No detailed help available for this command.`)
|
|
41
|
+
}
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!commands.has(commandName)) {
|
|
46
|
+
console.error(`Unknown command: ${commandName}`)
|
|
47
|
+
console.error(`Run "cnotes help" to see available commands.\n`)
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const def = commands.get(commandName)!
|
|
52
|
+
|
|
53
|
+
// `cnotes <command> --help` — show that command's help
|
|
54
|
+
if (wantsHelp) {
|
|
55
|
+
if (def.help) {
|
|
56
|
+
await renderMarkdown(container, def.help)
|
|
57
|
+
} else {
|
|
58
|
+
console.log(`${commandName} — ${def.description}\n`)
|
|
59
|
+
console.log(`No detailed help available for this command.`)
|
|
60
|
+
}
|
|
61
|
+
return
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Parse args from container.argv (minimist-parsed)
|
|
65
|
+
const { _: _positional, ...flags } = container.argv
|
|
66
|
+
let options = flags
|
|
67
|
+
|
|
68
|
+
if (def.argsSchema) {
|
|
69
|
+
const result = def.argsSchema.safeParse(flags)
|
|
70
|
+
if (!result.success) {
|
|
71
|
+
console.error(`Invalid options for "${commandName}":`)
|
|
72
|
+
for (const issue of result.error.issues) {
|
|
73
|
+
console.error(` ${issue.path.join('.')}: ${issue.message}`)
|
|
74
|
+
}
|
|
75
|
+
if (def.help) {
|
|
76
|
+
console.error(`\nRun "cnotes ${commandName} --help" for usage information.`)
|
|
77
|
+
}
|
|
78
|
+
process.exit(1)
|
|
79
|
+
}
|
|
80
|
+
options = result.data
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
await def.handler(options, { container })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
main().catch((err) => {
|
|
87
|
+
console.error(err)
|
|
88
|
+
process.exit(1)
|
|
89
|
+
})
|