dexin-content 0.2.0 → 0.3.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/cli/main.ts CHANGED
@@ -1,207 +1,197 @@
1
- #!/usr/bin/env tsx
2
- // ─────────────────────────────────────────────────────────────
3
- // dexin-content CLI main entry
4
- //
5
- // Modes:
6
- // Single-file: tsx cli/main.ts build <file.md> --domain lesson --out <path>
7
- // Directory: tsx cli/main.ts build <dir> --out <dir> [--manifest <path>]
8
- // ─────────────────────────────────────────────────────────────
9
-
10
- import { readFile, writeFile, mkdir } from 'node:fs/promises'
11
- import { existsSync } from 'node:fs'
12
- import { resolve, isAbsolute, dirname, basename } from 'node:path'
13
-
14
- import { DomainParserRegistry, compile } from '../core/compiler/compiler'
15
- import type { CompileInput } from '../core/compiler/compiler'
16
- import { lessonParser, buildLessonIdentity } from '../domains/lesson/index'
17
- import { documentParser, buildDocumentIdentity } from '../domains/document/index'
18
- import { flowBuild } from './build'
19
- import { flowPackage } from './package'
20
-
21
- type CmdArgs = Record<string, string | boolean>
22
-
23
- function parseArgs(argv: string[]): { command: string; args: CmdArgs; positionals: string[] } {
24
- const command = argv[0] ?? ''
25
- const positionals: string[] = []
26
- const args: CmdArgs = {}
27
- for (let i = 1; i < argv.length; i++) {
28
- const a = argv[i]
29
- if (a.startsWith('--')) {
30
- const key = a.slice(2)
31
- const next = argv[i + 1]
32
- if (next && !next.startsWith('--')) {
33
- args[key] = next; i++
34
- } else {
35
- args[key] = true
36
- }
37
- } else {
38
- positionals.push(a)
39
- }
40
- }
41
- return { command, args, positionals }
42
- }
43
-
44
- function printHelp() {
45
- console.log(`dexin-content CLI — Independent content compilation tool
46
-
47
- Usage:
48
- dexin-content <command> [options] (npm bin; or locally: npx tsx cli/main.ts)
49
-
50
- Commands:
51
- build Compile markdown to artifacts
52
- help Show this help
53
-
54
- Build modes:
55
- Single-file:
56
- dexin-content build <file.md> --domain <lesson|document> --out <path>
57
-
58
- Directory tree:
59
- dexin-content build <dir> --out <dir> [--manifest <path>] [--lessons <dir>] [--documents <dir>]
60
-
61
- Options:
62
- --domain <name> Domain parser (lesson | document). Required for single-file mode.
63
- --out <path> Output path (file for single, directory for batch).
64
- --manifest <path> Content manifest JSON for packaging.
65
- --lessons <dir> Relative path to lessons directory (default: lessons).
66
- --documents <dir> Relative path to documents directory.
67
- `)
68
- }
69
-
70
- async function runSingleFile(filePath: string, domain: string, outPath?: string) {
71
- const absPath = isAbsolute(filePath) ? filePath : resolve(filePath)
72
- if (!existsSync(absPath)) {
73
- console.error(`Error: file not found: ${absPath}`)
74
- process.exit(1)
75
- }
76
-
77
- const source = await readFile(absPath, 'utf-8')
78
- const relPath = basename(absPath)
79
-
80
- const registry = new DomainParserRegistry()
81
- registry.register(lessonParser)
82
- registry.register(documentParser)
83
-
84
- let identity: any
85
- let fixture: string
86
-
87
- if (domain === 'lesson') {
88
- // Try to parse lesson identity from parent directories
89
- const parent = dirname(absPath)
90
- const grandParent = dirname(parent)
91
- const topicSlug = basename(grandParent)
92
- const chapterSlug = basename(parent)
93
- const lessonSlug = basename(absPath).replace(/\.md$/, '')
94
- fixture = `${topicSlug}/${chapterSlug}/${lessonSlug}`
95
- identity = buildLessonIdentity(lessonSlug, topicSlug, chapterSlug, relPath)
96
- } else if (domain === 'document') {
97
- const slug = basename(absPath).replace(/\.md$/, '')
98
- fixture = slug
99
- identity = buildDocumentIdentity(slug, relPath)
100
- } else {
101
- console.error(`Error: unknown domain '${domain}'`)
102
- process.exit(1)
103
- }
104
-
105
- const input: CompileInput = {
106
- fixture,
107
- domain,
108
- identity,
109
- source,
110
- file: relPath
111
- }
112
-
113
- const result = compile(input, registry)
114
-
115
- if (result.kind === 'error') {
116
- const err = result.error
117
- console.error(`Compile error: ${err?.code ?? 'UNKNOWN'} — ${err?.message ?? err}`)
118
- process.exit(1)
119
- }
120
-
121
- const output = JSON.stringify(result.artifact, null, 2) + '\n'
122
-
123
- if (outPath) {
124
- const absOut = isAbsolute(outPath) ? outPath : resolve(outPath)
125
- await mkdir(dirname(absOut), { recursive: true })
126
- await writeFile(absOut, output, 'utf-8')
127
- console.log(`Compiled ${fixture} → ${absOut}`)
128
- } else {
129
- process.stdout.write(output)
130
- }
131
- }
132
-
133
- async function runDirectory(rootDir: string, args: CmdArgs) {
134
- const root = isAbsolute(rootDir) ? rootDir : resolve(rootDir)
135
- if (!existsSync(root)) {
136
- console.error(`Error: directory not found: ${root}`)
137
- process.exit(1)
138
- }
139
-
140
- const outDirRaw = args.out as string | undefined
141
- const outDir = outDirRaw
142
- ? (isAbsolute(outDirRaw) ? outDirRaw : resolve(outDirRaw))
143
- : resolve(root, 'output', 'flow')
144
-
145
- const registry = new DomainParserRegistry()
146
- registry.register(lessonParser)
147
- registry.register(documentParser)
148
-
149
- const lessonResult = await flowBuild({
150
- root,
151
- lessonsDir: args.lessons as string | undefined,
152
- documentDir: args.documents as string | undefined,
153
- outDir,
154
- domain: {
155
- lesson: lessonParser,
156
- document: documentParser,
157
- buildLessonIdentity
158
- }
159
- })
160
-
161
- console.log(`Build complete: ${lessonResult.compiled} artifacts in ${lessonResult.durationMs}ms`)
162
-
163
- if (args.manifest) {
164
- const manifestPath = isAbsolute(args.manifest as string)
165
- ? args.manifest as string
166
- : resolve(args.manifest as string)
167
- const pkg = await flowPackage({
168
- storeDir: outDir,
169
- manifestPath,
170
- outFile: resolve(outDir, 'manifest.json')
171
- })
172
- console.log(`Packaged manifest.json: ${pkg.lessons.length} lessons, ${pkg.pages.length} pages`)
173
- }
174
- }
175
-
176
- // ── Main ──────────────────────────────────────────────────
177
-
178
- const { command, args, positionals } = parseArgs(process.argv.slice(2))
179
-
180
- if (command === 'help' || !command) {
181
- printHelp()
182
- process.exit(0)
183
- }
184
-
185
- if (command === 'build') {
186
- const target = positionals[0]
187
- if (!target) {
188
- console.error('Error: missing target. Usage: build <file.md | directory>')
189
- process.exit(1)
190
- }
191
-
192
- const isFile = target.endsWith('.md')
193
- if (isFile) {
194
- const domain = args.domain as string | undefined
195
- if (!domain) {
196
- console.error('Error: --domain required for single-file mode')
197
- process.exit(1)
198
- }
199
- await runSingleFile(target, domain, args.out as string | undefined)
200
- } else {
201
- await runDirectory(target, args)
202
- }
203
- } else {
204
- console.error(`Unknown command: ${command}`)
205
- printHelp()
206
- process.exit(1)
207
- }
1
+ #!/usr/bin/env tsx
2
+ // ─────────────────────────────────────────────────────────────
3
+ // dexin-content CLI main entry
4
+ //
5
+ // Modes:
6
+ // Single-file: tsx cli/main.ts build <file.md> --domain lesson --out <path>
7
+ // Directory: tsx cli/main.ts build <dir> --out <dir> [--manifest <path>]
8
+ // ─────────────────────────────────────────────────────────────
9
+
10
+ import { readFile, writeFile, mkdir } from 'node:fs/promises'
11
+ import { existsSync } from 'node:fs'
12
+ import { resolve, isAbsolute, dirname, basename } from 'node:path'
13
+
14
+ import { DomainParserRegistry, compile } from '../core/compiler/compiler'
15
+ import type { CompileInput } from '../core/compiler/compiler'
16
+ import { lessonParser, buildLessonIdentity } from '../domains/lesson/index'
17
+ import { flowBuild } from './build'
18
+ import { flowPackage } from './package'
19
+
20
+ type CmdArgs = Record<string, string | boolean>
21
+
22
+ function parseArgs(argv: string[]): { command: string; args: CmdArgs; positionals: string[] } {
23
+ const command = argv[0] ?? ''
24
+ const positionals: string[] = []
25
+ const args: CmdArgs = {}
26
+ for (let i = 1; i < argv.length; i++) {
27
+ const a = argv[i]
28
+ if (a.startsWith('--')) {
29
+ const key = a.slice(2)
30
+ const next = argv[i + 1]
31
+ if (next && !next.startsWith('--')) {
32
+ args[key] = next; i++
33
+ } else {
34
+ args[key] = true
35
+ }
36
+ } else {
37
+ positionals.push(a)
38
+ }
39
+ }
40
+ return { command, args, positionals }
41
+ }
42
+
43
+ function printHelp() {
44
+ console.log(`dexin-content CLI — Independent content compilation tool
45
+
46
+ Usage:
47
+ dexin-content <command> [options] (npm bin; or locally: npx tsx cli/main.ts)
48
+
49
+ Commands:
50
+ build Compile markdown to artifacts
51
+ help Show this help
52
+
53
+ Build modes:
54
+ Single-file:
55
+ dexin-content build <file.md> --domain lesson --out <path>
56
+
57
+ Directory tree:
58
+ dexin-content build <dir> --out <dir> [--manifest <path>] [--lessons <dir>]
59
+
60
+ Options:
61
+ --domain <name> Domain parser (lesson). Required for single-file mode.
62
+ --out <path> Output path (file for single, directory for batch).
63
+ --manifest <path> Content manifest JSON for packaging.
64
+ --lessons <dir> Relative path to lessons directory (default: lessons).
65
+ `)
66
+ }
67
+
68
+ async function runSingleFile(filePath: string, domain: string, outPath?: string) {
69
+ const absPath = isAbsolute(filePath) ? filePath : resolve(filePath)
70
+ if (!existsSync(absPath)) {
71
+ console.error(`Error: file not found: ${absPath}`)
72
+ process.exit(1)
73
+ }
74
+
75
+ const source = await readFile(absPath, 'utf-8')
76
+ const relPath = basename(absPath)
77
+
78
+ const registry = new DomainParserRegistry()
79
+ registry.register(lessonParser)
80
+
81
+ let identity: any
82
+ let fixture: string
83
+
84
+ if (domain === 'lesson') {
85
+ // Try to parse lesson identity from parent directories
86
+ const parent = dirname(absPath)
87
+ const grandParent = dirname(parent)
88
+ const topicSlug = basename(grandParent)
89
+ const chapterSlug = basename(parent)
90
+ const lessonSlug = basename(absPath).replace(/\.md$/, '')
91
+ fixture = `${topicSlug}/${chapterSlug}/${lessonSlug}`
92
+ identity = buildLessonIdentity(lessonSlug, topicSlug, chapterSlug, relPath)
93
+ } else {
94
+ console.error(`Error: unknown domain '${domain}'`)
95
+ process.exit(1)
96
+ }
97
+
98
+ const input: CompileInput = {
99
+ fixture,
100
+ domain,
101
+ identity,
102
+ source,
103
+ file: relPath
104
+ }
105
+
106
+ const result = compile(input, registry)
107
+
108
+ if (result.kind === 'error') {
109
+ const err = result.error
110
+ console.error(`Compile error: ${err?.code ?? 'UNKNOWN'} — ${err?.message ?? err}`)
111
+ process.exit(1)
112
+ }
113
+
114
+ const output = JSON.stringify(result.artifact, null, 2) + '\n'
115
+
116
+ if (outPath) {
117
+ const absOut = isAbsolute(outPath) ? outPath : resolve(outPath)
118
+ await mkdir(dirname(absOut), { recursive: true })
119
+ await writeFile(absOut, output, 'utf-8')
120
+ console.log(`Compiled ${fixture} → ${absOut}`)
121
+ } else {
122
+ process.stdout.write(output)
123
+ }
124
+ }
125
+
126
+ async function runDirectory(rootDir: string, args: CmdArgs) {
127
+ const root = isAbsolute(rootDir) ? rootDir : resolve(rootDir)
128
+ if (!existsSync(root)) {
129
+ console.error(`Error: directory not found: ${root}`)
130
+ process.exit(1)
131
+ }
132
+
133
+ const outDirRaw = args.out as string | undefined
134
+ const outDir = outDirRaw
135
+ ? (isAbsolute(outDirRaw) ? outDirRaw : resolve(outDirRaw))
136
+ : resolve(root, 'output', 'flow')
137
+
138
+ const registry = new DomainParserRegistry()
139
+ registry.register(lessonParser)
140
+
141
+ const lessonResult = await flowBuild({
142
+ root,
143
+ lessonsDir: args.lessons as string | undefined,
144
+ outDir,
145
+ domain: {
146
+ lesson: lessonParser,
147
+ buildLessonIdentity
148
+ }
149
+ })
150
+
151
+ console.log(`Build complete: ${lessonResult.compiled} artifacts in ${lessonResult.durationMs}ms`)
152
+
153
+ if (args.manifest) {
154
+ const manifestPath = isAbsolute(args.manifest as string)
155
+ ? args.manifest as string
156
+ : resolve(args.manifest as string)
157
+ const pkg = await flowPackage({
158
+ storeDir: outDir,
159
+ manifestPath,
160
+ outFile: resolve(outDir, 'manifest.json')
161
+ })
162
+ console.log(`Packaged manifest.json: ${pkg.lessons.length} lessons, ${pkg.pages.length} pages`)
163
+ }
164
+ }
165
+
166
+ // ── Main ──────────────────────────────────────────────────
167
+
168
+ const { command, args, positionals } = parseArgs(process.argv.slice(2))
169
+
170
+ if (command === 'help' || !command) {
171
+ printHelp()
172
+ process.exit(0)
173
+ }
174
+
175
+ if (command === 'build') {
176
+ const target = positionals[0]
177
+ if (!target) {
178
+ console.error('Error: missing target. Usage: build <file.md | directory>')
179
+ process.exit(1)
180
+ }
181
+
182
+ const isFile = target.endsWith('.md')
183
+ if (isFile) {
184
+ const domain = args.domain as string | undefined
185
+ if (!domain) {
186
+ console.error('Error: --domain required for single-file mode')
187
+ process.exit(1)
188
+ }
189
+ await runSingleFile(target, domain, args.out as string | undefined)
190
+ } else {
191
+ await runDirectory(target, args)
192
+ }
193
+ } else {
194
+ console.error(`Unknown command: ${command}`)
195
+ printHelp()
196
+ process.exit(1)
197
+ }
@@ -73,7 +73,7 @@ const defaultFsOps: FsOps = {
73
73
 
74
74
  /**
75
75
  * File-system Artifact Store.
76
- * Layout: <baseDir>/index.json + <baseDir>/lessons/<id>.json
76
+ * Layout: <baseDir>/index.json + <baseDir>/content/<id>.json
77
77
  * Docs are serialised via toCanonicalJSON (2-space, key-lexicographic,
78
78
  * LF, trailing newline) to match golden candidate form.
79
79
  */
@@ -82,7 +82,7 @@ export function createFsArtifactStore (
82
82
  fsOps: FsOps = defaultFsOps
83
83
  ): ArtifactStore {
84
84
  const idxPath = () => `${baseDir}/index.json`
85
- const docPath = (id: string) => `${baseDir}/lessons/${id}.json`
85
+ const docPath = (id: string) => `${baseDir}/content/${id}.json`
86
86
 
87
87
  return {
88
88
  async writeIndex (index) {