@bfra.me/doc-sync 0.1.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.
Files changed (50) hide show
  1. package/README.md +288 -0
  2. package/lib/chunk-6NKAJT2M.js +1233 -0
  3. package/lib/chunk-DR6UG237.js +1027 -0
  4. package/lib/chunk-G5KKGJYO.js +1560 -0
  5. package/lib/chunk-ROLA7SBB.js +12 -0
  6. package/lib/cli/index.d.ts +1 -0
  7. package/lib/cli/index.js +397 -0
  8. package/lib/generators/index.d.ts +170 -0
  9. package/lib/generators/index.js +76 -0
  10. package/lib/index.d.ts +141 -0
  11. package/lib/index.js +118 -0
  12. package/lib/parsers/index.d.ts +264 -0
  13. package/lib/parsers/index.js +113 -0
  14. package/lib/types.d.ts +388 -0
  15. package/lib/types.js +7 -0
  16. package/package.json +99 -0
  17. package/src/cli/commands/index.ts +3 -0
  18. package/src/cli/commands/sync.ts +146 -0
  19. package/src/cli/commands/validate.ts +151 -0
  20. package/src/cli/commands/watch.ts +74 -0
  21. package/src/cli/index.ts +71 -0
  22. package/src/cli/types.ts +19 -0
  23. package/src/cli/ui.ts +123 -0
  24. package/src/generators/api-reference-generator.ts +268 -0
  25. package/src/generators/code-example-formatter.ts +313 -0
  26. package/src/generators/component-mapper.ts +383 -0
  27. package/src/generators/content-merger.ts +295 -0
  28. package/src/generators/frontmatter-generator.ts +277 -0
  29. package/src/generators/index.ts +56 -0
  30. package/src/generators/mdx-generator.ts +289 -0
  31. package/src/index.ts +131 -0
  32. package/src/orchestrator/index.ts +21 -0
  33. package/src/orchestrator/package-scanner.ts +276 -0
  34. package/src/orchestrator/sync-orchestrator.ts +382 -0
  35. package/src/orchestrator/validation-pipeline.ts +328 -0
  36. package/src/parsers/export-analyzer.ts +335 -0
  37. package/src/parsers/guards.ts +350 -0
  38. package/src/parsers/index.ts +82 -0
  39. package/src/parsers/jsdoc-extractor.ts +313 -0
  40. package/src/parsers/package-info.ts +267 -0
  41. package/src/parsers/readme-parser.ts +334 -0
  42. package/src/parsers/typescript-parser.ts +299 -0
  43. package/src/types.ts +423 -0
  44. package/src/utils/index.ts +13 -0
  45. package/src/utils/safe-patterns.ts +280 -0
  46. package/src/utils/sanitization.ts +164 -0
  47. package/src/watcher/change-detector.ts +138 -0
  48. package/src/watcher/debouncer.ts +168 -0
  49. package/src/watcher/file-watcher.ts +164 -0
  50. package/src/watcher/index.ts +27 -0
package/lib/types.d.ts ADDED
@@ -0,0 +1,388 @@
1
+ import { Result } from '@bfra.me/es/result';
2
+ import { z } from 'zod';
3
+
4
+ /**
5
+ * @bfra.me/doc-sync/types - Core type definitions for documentation synchronization
6
+ */
7
+
8
+ /**
9
+ * Metadata extracted from a package.json file
10
+ */
11
+ interface PackageInfo {
12
+ /** Package name from package.json */
13
+ readonly name: string;
14
+ /** Package version from package.json */
15
+ readonly version: string;
16
+ /** Package description from package.json */
17
+ readonly description?: string;
18
+ /** Keywords for categorization */
19
+ readonly keywords?: readonly string[];
20
+ /** Path to the package directory */
21
+ readonly packagePath: string;
22
+ /** Path to the package's source directory */
23
+ readonly srcPath: string;
24
+ /** Path to the package's README file if it exists */
25
+ readonly readmePath?: string;
26
+ /** Custom documentation config from package.json "docs" field */
27
+ readonly docsConfig?: DocConfigSource;
28
+ }
29
+ /**
30
+ * Source configuration for documentation, from package.json or docs.config.json
31
+ */
32
+ interface DocConfigSource {
33
+ /** Custom title override for the documentation page */
34
+ readonly title?: string;
35
+ /** Custom description override */
36
+ readonly description?: string;
37
+ /** Sidebar configuration */
38
+ readonly sidebar?: {
39
+ /** Label shown in sidebar navigation */
40
+ readonly label?: string;
41
+ /** Sort order in sidebar */
42
+ readonly order?: number;
43
+ /** Whether to hide from sidebar */
44
+ readonly hidden?: boolean;
45
+ };
46
+ /** Sections to exclude from auto-generation */
47
+ readonly excludeSections?: readonly string[];
48
+ /** Custom frontmatter fields to include */
49
+ readonly frontmatter?: Record<string, unknown>;
50
+ }
51
+ /**
52
+ * Configuration for documentation generation
53
+ */
54
+ interface DocConfig {
55
+ /** Root directory of the monorepo */
56
+ readonly rootDir: string;
57
+ /** Output directory for generated documentation */
58
+ readonly outputDir: string;
59
+ /** Glob patterns for packages to include */
60
+ readonly includePatterns: readonly string[];
61
+ /** Glob patterns for packages to exclude */
62
+ readonly excludePatterns?: readonly string[];
63
+ /** Whether to watch for changes */
64
+ readonly watch?: boolean;
65
+ /** Debounce delay in milliseconds for watch mode */
66
+ readonly debounceMs?: number;
67
+ }
68
+ /**
69
+ * Error types for parse operations
70
+ */
71
+ type ParseErrorCode = 'INVALID_SYNTAX' | 'FILE_NOT_FOUND' | 'READ_ERROR' | 'MALFORMED_JSON' | 'UNSUPPORTED_FORMAT';
72
+ /**
73
+ * Structured error for parse operations
74
+ */
75
+ interface ParseError {
76
+ /** Error classification code */
77
+ readonly code: ParseErrorCode;
78
+ /** Human-readable error message */
79
+ readonly message: string;
80
+ /** File path that caused the error */
81
+ readonly filePath?: string;
82
+ /** Line number where error occurred (1-indexed) */
83
+ readonly line?: number;
84
+ /** Column number where error occurred (1-indexed) */
85
+ readonly column?: number;
86
+ /** Original error if wrapping */
87
+ readonly cause?: unknown;
88
+ }
89
+ /**
90
+ * Result type for parse operations
91
+ */
92
+ type ParseResult<T> = Result<T, ParseError>;
93
+ /**
94
+ * Error types for sync operations
95
+ */
96
+ type SyncErrorCode = 'WRITE_ERROR' | 'VALIDATION_ERROR' | 'GENERATION_ERROR' | 'PACKAGE_NOT_FOUND' | 'CONFIG_ERROR';
97
+ /**
98
+ * Structured error for sync operations
99
+ */
100
+ interface SyncError {
101
+ /** Error classification code */
102
+ readonly code: SyncErrorCode;
103
+ /** Human-readable error message */
104
+ readonly message: string;
105
+ /** Package name that caused the error */
106
+ readonly packageName?: string;
107
+ /** File path that caused the error */
108
+ readonly filePath?: string;
109
+ /** Original error if wrapping */
110
+ readonly cause?: unknown;
111
+ }
112
+ /**
113
+ * Result type for sync operations
114
+ */
115
+ type SyncResult<T> = Result<T, SyncError>;
116
+ /**
117
+ * Information about a single sync operation
118
+ */
119
+ interface SyncInfo {
120
+ /** Package name that was synced */
121
+ readonly packageName: string;
122
+ /** Output file path */
123
+ readonly outputPath: string;
124
+ /** Whether the file was created or updated */
125
+ readonly action: 'created' | 'updated' | 'unchanged';
126
+ /** Timestamp of the sync operation */
127
+ readonly timestamp: Date;
128
+ }
129
+ /**
130
+ * Summary of a complete sync run
131
+ */
132
+ interface SyncSummary {
133
+ /** Total number of packages processed */
134
+ readonly totalPackages: number;
135
+ /** Number of successful syncs */
136
+ readonly successCount: number;
137
+ /** Number of failed syncs */
138
+ readonly failureCount: number;
139
+ /** Number of unchanged packages */
140
+ readonly unchangedCount: number;
141
+ /** Details of each sync operation */
142
+ readonly details: readonly SyncInfo[];
143
+ /** Errors encountered during sync */
144
+ readonly errors: readonly SyncError[];
145
+ /** Duration of the sync run in milliseconds */
146
+ readonly durationMs: number;
147
+ }
148
+ /**
149
+ * Extracted JSDoc information
150
+ */
151
+ interface JSDocInfo {
152
+ /** Main description from JSDoc */
153
+ readonly description?: string;
154
+ /** @param tags with name and description */
155
+ readonly params?: readonly JSDocParam[];
156
+ /** @returns description */
157
+ readonly returns?: string;
158
+ /** @example code blocks */
159
+ readonly examples?: readonly string[];
160
+ /** @deprecated message if present */
161
+ readonly deprecated?: string;
162
+ /** @since version if present */
163
+ readonly since?: string;
164
+ /** @see references */
165
+ readonly see?: readonly string[];
166
+ /** Custom tags not in standard set */
167
+ readonly customTags?: readonly JSDocTag[];
168
+ }
169
+ /**
170
+ * A single JSDoc @param entry
171
+ */
172
+ interface JSDocParam {
173
+ /** Parameter name */
174
+ readonly name: string;
175
+ /** Parameter type (from JSDoc or inferred) */
176
+ readonly type?: string;
177
+ /** Parameter description */
178
+ readonly description?: string;
179
+ /** Whether the parameter is optional */
180
+ readonly optional?: boolean;
181
+ /** Default value if specified */
182
+ readonly defaultValue?: string;
183
+ }
184
+ /**
185
+ * A custom JSDoc tag
186
+ */
187
+ interface JSDocTag {
188
+ /** Tag name without @ symbol */
189
+ readonly name: string;
190
+ /** Tag value/content */
191
+ readonly value?: string;
192
+ }
193
+ /**
194
+ * Information about an exported function
195
+ */
196
+ interface ExportedFunction {
197
+ /** Function name */
198
+ readonly name: string;
199
+ /** JSDoc documentation */
200
+ readonly jsdoc?: JSDocInfo;
201
+ /** Function signature as string */
202
+ readonly signature: string;
203
+ /** Whether it's async */
204
+ readonly isAsync: boolean;
205
+ /** Whether it's a generator */
206
+ readonly isGenerator: boolean;
207
+ /** Parameter information */
208
+ readonly parameters: readonly FunctionParameter[];
209
+ /** Return type as string */
210
+ readonly returnType: string;
211
+ /** Whether it's the default export */
212
+ readonly isDefault: boolean;
213
+ }
214
+ /**
215
+ * Function parameter information
216
+ */
217
+ interface FunctionParameter {
218
+ /** Parameter name */
219
+ readonly name: string;
220
+ /** TypeScript type */
221
+ readonly type: string;
222
+ /** Whether optional */
223
+ readonly optional: boolean;
224
+ /** Default value if provided */
225
+ readonly defaultValue?: string;
226
+ }
227
+ /**
228
+ * Information about an exported type or interface
229
+ */
230
+ interface ExportedType {
231
+ /** Type name */
232
+ readonly name: string;
233
+ /** JSDoc documentation */
234
+ readonly jsdoc?: JSDocInfo;
235
+ /** Full type definition as string */
236
+ readonly definition: string;
237
+ /** Kind of type declaration */
238
+ readonly kind: 'interface' | 'type' | 'enum' | 'class';
239
+ /** Whether it's the default export */
240
+ readonly isDefault: boolean;
241
+ /** Type parameters (generics) */
242
+ readonly typeParameters?: readonly string[];
243
+ }
244
+ /**
245
+ * Complete API surface extracted from a package
246
+ */
247
+ interface PackageAPI {
248
+ /** Exported functions */
249
+ readonly functions: readonly ExportedFunction[];
250
+ /** Exported types/interfaces */
251
+ readonly types: readonly ExportedType[];
252
+ /** Re-exported modules */
253
+ readonly reExports: readonly ReExport[];
254
+ }
255
+ /**
256
+ * Information about a re-export statement
257
+ */
258
+ interface ReExport {
259
+ /** Module path being re-exported from */
260
+ readonly from: string;
261
+ /** Named exports being re-exported, or '*' for all */
262
+ readonly exports: readonly string[] | '*';
263
+ /** Alias if renamed during re-export */
264
+ readonly alias?: string;
265
+ }
266
+ /**
267
+ * Parsed README content with sections
268
+ */
269
+ interface ReadmeContent {
270
+ /** Main title (first H1) */
271
+ readonly title?: string;
272
+ /** Content before first heading */
273
+ readonly preamble?: string;
274
+ /** Structured sections by heading */
275
+ readonly sections: readonly ReadmeSection[];
276
+ /** Raw markdown content */
277
+ readonly raw: string;
278
+ }
279
+ /**
280
+ * A section in the README
281
+ */
282
+ interface ReadmeSection {
283
+ /** Section heading text */
284
+ readonly heading: string;
285
+ /** Heading level (1-6) */
286
+ readonly level: number;
287
+ /** Section content (markdown) */
288
+ readonly content: string;
289
+ /** Nested subsections */
290
+ readonly children: readonly ReadmeSection[];
291
+ }
292
+ /**
293
+ * MDX frontmatter for Starlight
294
+ */
295
+ interface MDXFrontmatter {
296
+ /** Page title */
297
+ readonly title: string;
298
+ /** Page description for SEO */
299
+ readonly description?: string;
300
+ /** Sidebar configuration */
301
+ readonly sidebar?: {
302
+ readonly label?: string;
303
+ readonly order?: number;
304
+ readonly hidden?: boolean;
305
+ readonly badge?: string | {
306
+ readonly text: string;
307
+ readonly variant?: 'note' | 'tip' | 'caution' | 'danger' | 'success' | 'default';
308
+ };
309
+ };
310
+ /** Table of contents configuration */
311
+ readonly tableOfContents?: boolean | {
312
+ readonly minHeadingLevel?: number;
313
+ readonly maxHeadingLevel?: number;
314
+ };
315
+ /** Template to use */
316
+ readonly template?: 'doc' | 'splash';
317
+ /** Hero configuration for splash template */
318
+ readonly hero?: {
319
+ readonly title?: string;
320
+ readonly tagline?: string;
321
+ readonly image?: {
322
+ readonly src: string;
323
+ readonly alt: string;
324
+ };
325
+ readonly actions?: readonly {
326
+ readonly text: string;
327
+ readonly link: string;
328
+ readonly icon?: string;
329
+ readonly variant?: 'primary' | 'secondary' | 'minimal';
330
+ }[];
331
+ };
332
+ }
333
+ /**
334
+ * Generated MDX document
335
+ */
336
+ interface MDXDocument {
337
+ /** Frontmatter configuration */
338
+ readonly frontmatter: MDXFrontmatter;
339
+ /** Document body content */
340
+ readonly content: string;
341
+ /** Full rendered document */
342
+ readonly rendered: string;
343
+ }
344
+ /**
345
+ * Sentinel markers for content preservation
346
+ */
347
+ declare const SENTINEL_MARKERS: {
348
+ readonly AUTO_START: "{/* AUTO-GENERATED-START */}";
349
+ readonly AUTO_END: "{/* AUTO-GENERATED-END */}";
350
+ readonly MANUAL_START: "{/* MANUAL-CONTENT-START */}";
351
+ readonly MANUAL_END: "{/* MANUAL-CONTENT-END */}";
352
+ };
353
+ /**
354
+ * File change event from watcher
355
+ */
356
+ interface FileChangeEvent {
357
+ /** Type of change */
358
+ readonly type: 'add' | 'change' | 'unlink';
359
+ /** Absolute path to the changed file */
360
+ readonly path: string;
361
+ /** Package name if determinable */
362
+ readonly packageName?: string;
363
+ /** Timestamp of the change */
364
+ readonly timestamp: Date;
365
+ }
366
+ /**
367
+ * Options for the CLI
368
+ */
369
+ interface CLIOptions {
370
+ /** Root directory to operate from */
371
+ readonly root?: string;
372
+ /** Specific packages to sync */
373
+ readonly packages?: readonly string[];
374
+ /** Run in watch mode */
375
+ readonly watch?: boolean;
376
+ /** Dry run without writing files */
377
+ readonly dryRun?: boolean;
378
+ /** Verbose logging */
379
+ readonly verbose?: boolean;
380
+ /** Quiet mode (minimal output) */
381
+ readonly quiet?: boolean;
382
+ }
383
+ /**
384
+ * Zod schema type helper for runtime validation
385
+ */
386
+ type InferSchema<T extends z.ZodTypeAny> = z.infer<T>;
387
+
388
+ export { type CLIOptions, type DocConfig, type DocConfigSource, type ExportedFunction, type ExportedType, type FileChangeEvent, type FunctionParameter, type InferSchema, type JSDocInfo, type JSDocParam, type JSDocTag, type MDXDocument, type MDXFrontmatter, type PackageAPI, type PackageInfo, type ParseError, type ParseErrorCode, type ParseResult, type ReExport, type ReadmeContent, type ReadmeSection, SENTINEL_MARKERS, type SyncError, type SyncErrorCode, type SyncInfo, type SyncResult, type SyncSummary };
package/lib/types.js ADDED
@@ -0,0 +1,7 @@
1
+ import {
2
+ SENTINEL_MARKERS
3
+ } from "./chunk-ROLA7SBB.js";
4
+ export {
5
+ SENTINEL_MARKERS
6
+ };
7
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "name": "@bfra.me/doc-sync",
3
+ "version": "0.1.0",
4
+ "description": "Intelligent documentation synchronization engine for automatic Astro Starlight site updates",
5
+ "keywords": [
6
+ "astro",
7
+ "bfra.me",
8
+ "documentation",
9
+ "generator",
10
+ "jsdoc",
11
+ "mdx",
12
+ "starlight",
13
+ "sync",
14
+ "typescript",
15
+ "works"
16
+ ],
17
+ "homepage": "https://github.com/bfra-me/works/tree/main/packages/doc-sync#readme",
18
+ "bugs": "https://github.com/bfra-me/works/issues",
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/bfra-me/works.git",
22
+ "directory": "packages/doc-sync"
23
+ },
24
+ "license": "MIT",
25
+ "author": "Marcus R. Brown <contact@bfra.me>",
26
+ "type": "module",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./lib/index.d.ts",
30
+ "source": "./src/index.ts",
31
+ "import": "./lib/index.js"
32
+ },
33
+ "./generators": {
34
+ "types": "./lib/generators/index.d.ts",
35
+ "source": "./src/generators/index.ts",
36
+ "import": "./lib/generators/index.js"
37
+ },
38
+ "./parsers": {
39
+ "types": "./lib/parsers/index.d.ts",
40
+ "source": "./src/parsers/index.ts",
41
+ "import": "./lib/parsers/index.js"
42
+ },
43
+ "./types": {
44
+ "types": "./lib/types.d.ts",
45
+ "source": "./src/types.ts",
46
+ "import": "./lib/types.js"
47
+ },
48
+ "./package.json": "./package.json"
49
+ },
50
+ "main": "./lib/index.js",
51
+ "types": "./lib/index.d.ts",
52
+ "bin": {
53
+ "doc-sync": "./lib/cli/index.js"
54
+ },
55
+ "files": [
56
+ "!**/*.map",
57
+ "lib",
58
+ "src"
59
+ ],
60
+ "dependencies": {
61
+ "@clack/prompts": "0.11.0",
62
+ "cac": "6.7.14",
63
+ "consola": "3.4.2",
64
+ "escape-html": "^1.0.3",
65
+ "fast-glob": "3.3.3",
66
+ "remark-mdx": "3.1.1",
67
+ "remark-parse": "11.0.0",
68
+ "ts-morph": "26.0.0",
69
+ "unified": "11.0.5",
70
+ "zod": "4.1.13",
71
+ "@bfra.me/es": "0.1.0"
72
+ },
73
+ "devDependencies": {
74
+ "@types/escape-html": "^1.0.4",
75
+ "chokidar": "5.0.0",
76
+ "memfs": "4.51.1",
77
+ "@bfra.me/works": "0.0.0-development"
78
+ },
79
+ "peerDependencies": {
80
+ "chokidar": "^5.0.0"
81
+ },
82
+ "peerDependenciesMeta": {
83
+ "chokidar": {
84
+ "optional": true
85
+ }
86
+ },
87
+ "publishConfig": {
88
+ "access": "public",
89
+ "provenance": true
90
+ },
91
+ "scripts": {
92
+ "build": "tsup",
93
+ "dev": "tsx src/cli/index.ts",
94
+ "start": "node lib/cli/index.js",
95
+ "test": "vitest run",
96
+ "lint": "eslint",
97
+ "test:watch": "vitest"
98
+ }
99
+ }
@@ -0,0 +1,3 @@
1
+ export {runSync} from './sync.js'
2
+ export {runValidate} from './validate.js'
3
+ export {runWatch} from './watch.js'
@@ -0,0 +1,146 @@
1
+ import type {SyncError} from '../../types.js'
2
+ import type {GlobalOptions, PackageSelectionOption} from '../types.js'
3
+ import type {Logger} from '../ui.js'
4
+
5
+ import path from 'node:path'
6
+ import process from 'node:process'
7
+
8
+ import {createPackageScanner} from '../../orchestrator/package-scanner.js'
9
+ import {createSyncOrchestrator} from '../../orchestrator/sync-orchestrator.js'
10
+ import {
11
+ createLogger,
12
+ createProgressCallback,
13
+ createSpinner,
14
+ formatDuration,
15
+ formatPackageList,
16
+ handleCancel,
17
+ selectPackages,
18
+ showCancel,
19
+ showIntro,
20
+ showOutro,
21
+ } from '../ui.js'
22
+
23
+ export async function runSync(packages: string[], options: GlobalOptions): Promise<void> {
24
+ const logger = createLogger(options)
25
+ const rootDir = path.resolve(options.root)
26
+ const outputDir = path.join(rootDir, 'docs', 'src', 'content', 'docs', 'packages')
27
+
28
+ if (options.quiet !== true) {
29
+ showIntro('📚 doc-sync')
30
+ }
31
+
32
+ let selectedPackages: readonly string[] = packages
33
+
34
+ if (options.interactive === true && packages.length === 0) {
35
+ const scanner = createPackageScanner({
36
+ rootDir,
37
+ parseSourceFiles: false,
38
+ parseReadme: false,
39
+ })
40
+
41
+ const scanResult = await scanner.scan()
42
+ const availablePackages: PackageSelectionOption[] = scanResult.packages.map(pkg => ({
43
+ value: pkg.info.name,
44
+ label: pkg.info.name,
45
+ hint: pkg.needsDocumentation ? 'needs docs' : 'up to date',
46
+ }))
47
+
48
+ const selection = await selectPackages(availablePackages)
49
+
50
+ if (handleCancel(selection)) {
51
+ showCancel()
52
+ process.exit(0)
53
+ }
54
+
55
+ selectedPackages = selection
56
+ }
57
+
58
+ const spinner = options.quiet === true ? undefined : createSpinner()
59
+ const errors: SyncError[] = []
60
+
61
+ const orchestrator = createSyncOrchestrator({
62
+ config: {
63
+ rootDir,
64
+ outputDir,
65
+ includePatterns: ['packages/*'],
66
+ excludePatterns: [],
67
+ },
68
+ dryRun: options.dryRun ?? false,
69
+ verbose: options.verbose ?? false,
70
+ onProgress: createProgressCallback(options),
71
+ onError(error: SyncError): void {
72
+ errors.push(error)
73
+ logger.error(`Error syncing ${error.packageName ?? 'unknown'}: ${error.message}`)
74
+ },
75
+ })
76
+
77
+ const actionPrefix = options.dryRun === true ? '[DRY RUN] ' : ''
78
+
79
+ if (selectedPackages.length > 0) {
80
+ spinner?.start(`${actionPrefix}Syncing ${formatPackageList(selectedPackages)}...`)
81
+ const result = await orchestrator.syncPackages(selectedPackages)
82
+ spinner?.stop(`${actionPrefix}Sync complete`)
83
+
84
+ reportSyncResult(result, logger, options)
85
+ } else {
86
+ spinner?.start(`${actionPrefix}Syncing all packages...`)
87
+ const result = await orchestrator.syncAll()
88
+ spinner?.stop(`${actionPrefix}Sync complete`)
89
+
90
+ reportSyncResult(result, logger, options)
91
+ }
92
+
93
+ if (errors.length > 0) {
94
+ if (options.quiet !== true) {
95
+ showOutro(`⚠️ Completed with ${errors.length} error${errors.length === 1 ? '' : 's'}`)
96
+ }
97
+ process.exit(1)
98
+ }
99
+
100
+ if (options.quiet !== true) {
101
+ showOutro('✨ Documentation sync complete!')
102
+ }
103
+ }
104
+
105
+ interface SyncResult {
106
+ readonly totalPackages: number
107
+ readonly successCount: number
108
+ readonly failureCount: number
109
+ readonly unchangedCount: number
110
+ readonly durationMs: number
111
+ readonly details: readonly {
112
+ readonly packageName: string
113
+ readonly action: 'created' | 'updated' | 'unchanged'
114
+ readonly outputPath: string
115
+ }[]
116
+ }
117
+
118
+ function reportSyncResult(result: SyncResult, logger: Logger, options: GlobalOptions): void {
119
+ const {successCount, failureCount, unchangedCount, durationMs, details} = result
120
+
121
+ if (options.verbose === true) {
122
+ for (const detail of details) {
123
+ switch (detail.action) {
124
+ case 'created':
125
+ logger.success(`Created: ${detail.packageName}`)
126
+ break
127
+ case 'updated':
128
+ logger.info(`Updated: ${detail.packageName}`)
129
+ break
130
+ case 'unchanged':
131
+ logger.debug(`Unchanged: ${detail.packageName}`)
132
+ break
133
+ }
134
+ }
135
+ }
136
+
137
+ const summary = [
138
+ successCount > 0 ? `${successCount} synced` : null,
139
+ unchangedCount > 0 ? `${unchangedCount} unchanged` : null,
140
+ failureCount > 0 ? `${failureCount} failed` : null,
141
+ ]
142
+ .filter(Boolean)
143
+ .join(', ')
144
+
145
+ logger.info(`${summary} in ${formatDuration(durationMs)}`)
146
+ }