|
7 | 7 | package obsidian |
8 | 8 |
|
9 | 9 | import ( |
10 | | - "fmt" |
11 | | - "os" |
12 | 10 | "path/filepath" |
13 | 11 |
|
14 | 12 | "github.com/spf13/cobra" |
15 | 13 |
|
16 | | - "github.com/ActiveMemory/ctx/internal/assets/tpl" |
17 | | - "github.com/ActiveMemory/ctx/internal/cli/journal/core/consolidate" |
18 | | - "github.com/ActiveMemory/ctx/internal/cli/journal/core/format" |
19 | | - "github.com/ActiveMemory/ctx/internal/cli/journal/core/frontmatter" |
20 | | - "github.com/ActiveMemory/ctx/internal/cli/journal/core/moc" |
21 | | - "github.com/ActiveMemory/ctx/internal/cli/journal/core/parse" |
22 | | - "github.com/ActiveMemory/ctx/internal/cli/journal/core/reduce" |
23 | | - "github.com/ActiveMemory/ctx/internal/cli/journal/core/section" |
24 | | - "github.com/ActiveMemory/ctx/internal/cli/journal/core/turn" |
25 | | - "github.com/ActiveMemory/ctx/internal/cli/journal/core/wikilink" |
26 | | - "github.com/ActiveMemory/ctx/internal/cli/journal/core/wrap" |
| 14 | + coreObsidian "github.com/ActiveMemory/ctx/internal/cli/journal/core/obsidian" |
27 | 15 | "github.com/ActiveMemory/ctx/internal/config/dir" |
28 | | - "github.com/ActiveMemory/ctx/internal/config/file" |
29 | | - "github.com/ActiveMemory/ctx/internal/config/fs" |
30 | | - "github.com/ActiveMemory/ctx/internal/config/obsidian" |
31 | | - errFs "github.com/ActiveMemory/ctx/internal/err/fs" |
32 | | - errJournal "github.com/ActiveMemory/ctx/internal/err/journal" |
33 | | - "github.com/ActiveMemory/ctx/internal/io" |
34 | 16 | "github.com/ActiveMemory/ctx/internal/rc" |
35 | | - "github.com/ActiveMemory/ctx/internal/write/err" |
36 | | - writeObsidian "github.com/ActiveMemory/ctx/internal/write/obsidian" |
37 | 17 | ) |
38 | 18 |
|
39 | 19 | // Run generates an Obsidian vault from journal entries. |
40 | 20 | // |
41 | | -// Pipeline: |
42 | | -// 1. Scan entries (reuse core.ScanJournalEntries) |
43 | | -// 2. Create output dirs (entries/, topics/, files/, types/, .obsidian/) |
44 | | -// 3. Write .obsidian/app.json |
45 | | -// 4. Transform and write entries (normalize, convert links, transform |
46 | | -// frontmatter, add related footer) |
47 | | -// 5. Build indices (reuse core.BuildTopicIndex etc.) |
48 | | -// 6. Generate and write MOC pages |
49 | | -// 7. Generate and write Home.md |
50 | | -// |
51 | 21 | // Parameters: |
52 | 22 | // - cmd: Cobra command for output stream |
53 | 23 | // - output: Output directory for the vault |
54 | 24 | // |
55 | 25 | // Returns: |
56 | 26 | // - error: Non-nil if generation fails |
57 | 27 | func Run(cmd *cobra.Command, output string) error { |
58 | | - return BuildVault( |
| 28 | + return coreObsidian.BuildVault( |
59 | 29 | cmd, filepath.Join(rc.ContextDir(), dir.Journal), output, |
60 | 30 | ) |
61 | 31 | } |
62 | | - |
63 | | -// BuildVault generates an Obsidian vault from journal entries in |
64 | | -// journalDir and writes the output to the output directory. |
65 | | -// |
66 | | -// Parameters: |
67 | | -// - cmd: Cobra command for output stream |
68 | | -// - journalDir: Path to the source journal directory |
69 | | -// - output: Output directory for the vault |
70 | | -// |
71 | | -// Returns: |
72 | | -// - error: Non-nil if generation fails |
73 | | -func BuildVault(cmd *cobra.Command, journalDir, output string) error { |
74 | | - if _, statErr := os.Stat(journalDir); os.IsNotExist(statErr) { |
75 | | - return errJournal.NoDir(journalDir) |
76 | | - } |
77 | | - |
78 | | - entries, scanErr := parse.ScanJournalEntries(journalDir) |
79 | | - if scanErr != nil { |
80 | | - return errJournal.Scan(scanErr) |
81 | | - } |
82 | | - |
83 | | - if len(entries) == 0 { |
84 | | - return errJournal.NoEntries(journalDir) |
85 | | - } |
86 | | - |
87 | | - // Create output directory structure |
88 | | - dirs := []string{ |
89 | | - output, |
90 | | - filepath.Join(output, obsidian.DirEntries), |
91 | | - filepath.Join(output, obsidian.DirConfig), |
92 | | - filepath.Join(output, dir.JournTopics), |
93 | | - filepath.Join(output, dir.JournalFiles), |
94 | | - filepath.Join(output, dir.JournalTypes), |
95 | | - } |
96 | | - for _, d := range dirs { |
97 | | - if mkErr := io.SafeMkdirAll(d, fs.PermExec); mkErr != nil { |
98 | | - return errFs.Mkdir(d, mkErr) |
99 | | - } |
100 | | - } |
101 | | - |
102 | | - // Write .obsidian/app.json |
103 | | - appConfigPath := filepath.Join( |
104 | | - output, obsidian.DirConfig, obsidian.AppConfigFile, |
105 | | - ) |
106 | | - if writeErr := io.SafeWriteFile( |
107 | | - appConfigPath, []byte(obsidian.AppConfig), fs.PermFile, |
108 | | - ); writeErr != nil { |
109 | | - return errFs.FileWrite(appConfigPath, writeErr) |
110 | | - } |
111 | | - |
112 | | - // Write README |
113 | | - readmePath := filepath.Join(output, file.Readme) |
114 | | - if writeErr := io.SafeWriteFile( |
115 | | - readmePath, |
116 | | - []byte(fmt.Sprintf(tpl.ObsidianReadme, journalDir)), |
117 | | - fs.PermFile, |
118 | | - ); writeErr != nil { |
119 | | - return errFs.FileWrite(readmePath, writeErr) |
120 | | - } |
121 | | - |
122 | | - // Build indices for MOC pages and related footer |
123 | | - regularEntries := moc.FilterRegularEntries(entries) |
124 | | - |
125 | | - topicEntries := moc.FilterEntriesWithTopics(entries) |
126 | | - topics := section.BuildTopicIndex(topicEntries) |
127 | | - |
128 | | - keyFileEntries := moc.FilterEntriesWithKeyFiles(entries) |
129 | | - keyFiles := section.BuildKeyFileIndex(keyFileEntries) |
130 | | - |
131 | | - typeEntries := moc.FilterEntriesWithType(entries) |
132 | | - sessionTypes := section.BuildTypeIndex(typeEntries) |
133 | | - |
134 | | - // Build topic lookup for related footer |
135 | | - topicIndex := moc.BuildTopicLookup(topicEntries) |
136 | | - |
137 | | - // Transform and write entries |
138 | | - for _, entry := range entries { |
139 | | - src := entry.Path |
140 | | - dst := filepath.Join(output, obsidian.DirEntries, entry.Filename) |
141 | | - |
142 | | - content, readErr := io.SafeReadUserFile(filepath.Clean(src)) |
143 | | - if readErr != nil { |
144 | | - err.WarnFile(cmd, entry.Filename, readErr) |
145 | | - continue |
146 | | - } |
147 | | - |
148 | | - // Normalize content (read-only - do NOT write back to source) |
149 | | - normalized := wrap.Content( |
150 | | - turn.MergeConsecutive( |
151 | | - consolidate.ToolRuns( |
152 | | - reduce.CleanToolOutputJSON( |
153 | | - reduce.StripSystemReminders(string(content)), |
154 | | - ), |
155 | | - ), |
156 | | - ), |
157 | | - ) |
158 | | - |
159 | | - // Transform for Obsidian |
160 | | - sourcePath := filepath.Join( |
161 | | - dir.Context, dir.Journal, entry.Filename, |
162 | | - ) |
163 | | - transformed := frontmatter.Transform(normalized, sourcePath) |
164 | | - transformed = wikilink.ConvertMarkdownLinks(transformed) |
165 | | - transformed += moc.GenerateRelatedFooter( |
166 | | - entry, topicIndex, obsidian.MaxRelated, |
167 | | - ) |
168 | | - |
169 | | - if writeErr := io.SafeWriteFile( |
170 | | - dst, []byte(transformed), fs.PermFile, |
171 | | - ); writeErr != nil { |
172 | | - err.WarnFile(cmd, entry.Filename, writeErr) |
173 | | - continue |
174 | | - } |
175 | | - } |
176 | | - |
177 | | - // Write topic MOC and pages |
178 | | - if len(topics) > 0 { |
179 | | - topicsDir := filepath.Join(output, dir.JournTopics) |
180 | | - mocPath := filepath.Join(output, obsidian.MOCTopics) |
181 | | - if writeErr := io.SafeWriteFile( |
182 | | - mocPath, []byte(moc.ObsidianTopics(topics)), |
183 | | - fs.PermFile, |
184 | | - ); writeErr != nil { |
185 | | - return errFs.FileWrite(mocPath, writeErr) |
186 | | - } |
187 | | - |
188 | | - for _, t := range topics { |
189 | | - if !t.Popular { |
190 | | - continue |
191 | | - } |
192 | | - pagePath := filepath.Join(topicsDir, t.Name+file.ExtMarkdown) |
193 | | - if writeErr := io.SafeWriteFile( |
194 | | - pagePath, []byte(moc.GenerateObsidianTopicPage(t)), |
195 | | - fs.PermFile, |
196 | | - ); writeErr != nil { |
197 | | - err.WarnFile(cmd, pagePath, writeErr) |
198 | | - } |
199 | | - } |
200 | | - } |
201 | | - |
202 | | - // Write key files MOC and pages |
203 | | - if len(keyFiles) > 0 { |
204 | | - filesDir := filepath.Join(output, dir.JournalFiles) |
205 | | - mocPath := filepath.Join(output, obsidian.MOCFiles) |
206 | | - if writeErr := io.SafeWriteFile( |
207 | | - mocPath, []byte(moc.ObsidianFiles(keyFiles)), |
208 | | - fs.PermFile, |
209 | | - ); writeErr != nil { |
210 | | - return errFs.FileWrite(mocPath, writeErr) |
211 | | - } |
212 | | - |
213 | | - for _, kf := range keyFiles { |
214 | | - if !kf.Popular { |
215 | | - continue |
216 | | - } |
217 | | - slug := format.KeyFileSlug(kf.Path) |
218 | | - pagePath := filepath.Join(filesDir, slug+file.ExtMarkdown) |
219 | | - if writeErr := io.SafeWriteFile( |
220 | | - pagePath, []byte(moc.GenerateObsidianFilePage(kf)), |
221 | | - fs.PermFile, |
222 | | - ); writeErr != nil { |
223 | | - err.WarnFile(cmd, pagePath, writeErr) |
224 | | - } |
225 | | - } |
226 | | - } |
227 | | - |
228 | | - // Write types MOC and pages |
229 | | - if len(sessionTypes) > 0 { |
230 | | - typesDir := filepath.Join(output, dir.JournalTypes) |
231 | | - mocPath := filepath.Join(output, obsidian.MOCTypes) |
232 | | - if writeErr := io.SafeWriteFile( |
233 | | - mocPath, []byte(moc.ObsidianTypes(sessionTypes)), |
234 | | - fs.PermFile, |
235 | | - ); writeErr != nil { |
236 | | - return errFs.FileWrite(mocPath, writeErr) |
237 | | - } |
238 | | - |
239 | | - for _, st := range sessionTypes { |
240 | | - pagePath := filepath.Join(typesDir, st.Name+file.ExtMarkdown) |
241 | | - if writeErr := io.SafeWriteFile( |
242 | | - pagePath, |
243 | | - []byte(moc.GenerateObsidianTypePage(st)), fs.PermFile, |
244 | | - ); writeErr != nil { |
245 | | - err.WarnFile(cmd, pagePath, writeErr) |
246 | | - } |
247 | | - } |
248 | | - } |
249 | | - |
250 | | - // Write Home.md |
251 | | - homePath := filepath.Join(output, obsidian.MOCHome) |
252 | | - if writeErr := io.SafeWriteFile( |
253 | | - homePath, |
254 | | - []byte(moc.Home( |
255 | | - regularEntries, |
256 | | - len(topics) > 0, len(keyFiles) > 0, len(sessionTypes) > 0, |
257 | | - )), |
258 | | - fs.PermFile, |
259 | | - ); writeErr != nil { |
260 | | - return errFs.FileWrite(homePath, writeErr) |
261 | | - } |
262 | | - |
263 | | - writeObsidian.InfoGenerated(cmd, len(entries), output) |
264 | | - |
265 | | - return nil |
266 | | -} |
0 commit comments