@thegetty/quire-cli 1.0.0-rc.35 → 1.0.0-rc.37

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 (159) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +9 -0
  3. package/bin/cli.js +19 -1
  4. package/package.json +21 -8
  5. package/patches/README.md +19 -0
  6. package/patches/install-npm-version+1.0.9.patch +12119 -0
  7. package/schemas/objects.schema.json +12 -38
  8. package/schemas/publication.schema.json +0 -22
  9. package/schemas/references.schema.json +0 -1
  10. package/src/Command.js +26 -6
  11. package/src/Command.spec.js +99 -0
  12. package/src/commands/README.md +213 -122
  13. package/src/commands/build.js +37 -36
  14. package/src/commands/build.spec.js +113 -0
  15. package/src/commands/build.test.js +397 -0
  16. package/src/commands/clean.js +25 -19
  17. package/src/commands/clean.spec.js +108 -0
  18. package/src/commands/clean.test.js +260 -0
  19. package/src/commands/config.js +251 -0
  20. package/src/commands/config.spec.js +107 -0
  21. package/src/commands/config.test.js +715 -0
  22. package/src/commands/create.js +42 -14
  23. package/src/commands/create.spec.js +112 -0
  24. package/src/commands/create.test.js +415 -0
  25. package/src/commands/epub.js +59 -30
  26. package/src/commands/epub.spec.js +114 -0
  27. package/src/commands/epub.test.js +503 -0
  28. package/src/commands/index.js +9 -2
  29. package/src/commands/info.js +18 -24
  30. package/src/commands/info.spec.js +64 -0
  31. package/src/commands/info.test.js +415 -0
  32. package/src/commands/pdf.js +58 -84
  33. package/src/commands/pdf.spec.js +114 -0
  34. package/src/commands/pdf.test.js +464 -0
  35. package/src/commands/preview.js +32 -31
  36. package/src/commands/preview.spec.js +97 -0
  37. package/src/commands/preview.test.js +250 -0
  38. package/src/commands/use.js +56 -0
  39. package/src/commands/use.spec.js +61 -0
  40. package/src/commands/use.test.js +280 -0
  41. package/src/commands/validate.js +31 -19
  42. package/src/commands/validate.spec.js +82 -0
  43. package/src/commands/validate.test.js +234 -0
  44. package/src/commands/workflows.js +70 -0
  45. package/src/errors/build/build-failed-error.js +19 -0
  46. package/src/errors/build/config-field-missing-error.js +20 -0
  47. package/src/errors/build/config-file-not-found-error.js +20 -0
  48. package/src/errors/build/index.js +11 -0
  49. package/src/errors/index.js +48 -0
  50. package/src/errors/install/dependency-install-error.js +19 -0
  51. package/src/errors/install/directory-not-empty-error.js +25 -0
  52. package/src/errors/install/index.js +12 -0
  53. package/src/errors/install/invalid-path-error.js +31 -0
  54. package/src/errors/install/invalid-starter-error.js +27 -0
  55. package/src/errors/install/version-not-found-error.js +35 -0
  56. package/src/errors/output/epub-generation-error.js +19 -0
  57. package/src/errors/output/index.js +14 -0
  58. package/src/errors/output/invalid-epub-library-error.js +20 -0
  59. package/src/errors/output/invalid-pdf-library-error.js +20 -0
  60. package/src/errors/output/missing-build-output-error.js +21 -0
  61. package/src/errors/output/pdf-generation-error.js +24 -0
  62. package/src/errors/output/tool-not-found-error.js +37 -0
  63. package/src/errors/project/index.js +10 -0
  64. package/src/errors/project/not-in-project-error.js +20 -0
  65. package/src/errors/project/project-create-error.js +21 -0
  66. package/src/errors/quire-error.js +27 -0
  67. package/src/errors/validation/validation-error.js +20 -11
  68. package/src/helpers/clean.js +1 -1
  69. package/src/helpers/docs-url.js +32 -0
  70. package/src/helpers/test-cwd.js +5 -6
  71. package/src/helpers/test-cwd.test.js +192 -0
  72. package/src/helpers/which.js +10 -4
  73. package/src/lib/11ty/README.md +135 -19
  74. package/src/lib/11ty/api.js +196 -94
  75. package/src/lib/11ty/cli.js +91 -37
  76. package/src/lib/11ty/index.js +64 -5
  77. package/src/lib/11ty/index.test.js +655 -0
  78. package/src/lib/README.md +275 -0
  79. package/src/lib/commander/index.js +100 -0
  80. package/src/lib/commander/index.test.js +86 -0
  81. package/src/lib/commander/options.js +195 -0
  82. package/src/lib/commander/options.test.js +109 -0
  83. package/src/lib/conf/README.md +84 -73
  84. package/src/lib/conf/config.js +5 -3
  85. package/src/lib/conf/config.test.js +281 -0
  86. package/src/lib/conf/defaults.js +44 -0
  87. package/src/lib/conf/format.js +60 -0
  88. package/src/lib/conf/format.test.js +106 -0
  89. package/src/lib/conf/helpers.js +91 -0
  90. package/src/lib/conf/helpers.test.js +136 -0
  91. package/src/lib/conf/index.js +22 -0
  92. package/src/lib/conf/schema.js +53 -8
  93. package/src/lib/epub/README.md +133 -2
  94. package/src/lib/epub/engines.js +46 -0
  95. package/src/lib/epub/epub.js +36 -11
  96. package/src/lib/epub/index.js +111 -21
  97. package/src/lib/epub/index.test.js +518 -0
  98. package/src/lib/epub/pandoc.js +33 -4
  99. package/src/lib/epub/pandoc.test.js +122 -0
  100. package/src/lib/epub/schema.js +21 -0
  101. package/src/lib/error/README.md +170 -0
  102. package/src/lib/error/handler.js +107 -0
  103. package/src/lib/git/README.md +151 -2
  104. package/src/lib/git/index.js +217 -13
  105. package/src/lib/git/index.spec.js +80 -0
  106. package/src/lib/git/index.test.js +453 -0
  107. package/src/lib/installer/index.js +309 -0
  108. package/src/lib/installer/index.spec.js +83 -0
  109. package/src/lib/installer/index.test.js +545 -0
  110. package/src/lib/logger/README.md +424 -0
  111. package/src/lib/logger/debug.js +132 -0
  112. package/src/lib/logger/debug.spec.js +130 -0
  113. package/src/lib/logger/debug.test.js +128 -0
  114. package/src/lib/logger/index.js +228 -0
  115. package/src/lib/logger/index.spec.js +131 -0
  116. package/src/lib/logger/index.test.js +477 -0
  117. package/src/lib/npm/README.md +127 -0
  118. package/src/lib/npm/index.js +198 -0
  119. package/src/lib/npm/index.spec.js +60 -0
  120. package/src/lib/npm/index.test.js +355 -0
  121. package/src/lib/pdf/README.md +131 -0
  122. package/src/lib/pdf/engines.js +46 -0
  123. package/src/lib/pdf/index.js +124 -21
  124. package/src/lib/pdf/index.test.js +708 -0
  125. package/src/lib/pdf/paged.js +100 -52
  126. package/src/lib/pdf/paged.test.js +366 -0
  127. package/src/lib/pdf/prince.js +115 -37
  128. package/src/lib/pdf/prince.test.js +202 -0
  129. package/src/lib/pdf/schema.js +21 -0
  130. package/src/lib/pdf/split.js +61 -33
  131. package/src/lib/pdf/split.test.js +445 -0
  132. package/src/lib/process/manager.js +110 -0
  133. package/src/lib/process/manager.test.js +55 -0
  134. package/src/lib/project/build.js +143 -0
  135. package/src/lib/project/build.test.js +253 -0
  136. package/src/lib/project/config.js +48 -0
  137. package/src/lib/project/config.test.js +134 -0
  138. package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
  139. package/src/lib/project/detect.test.js +157 -0
  140. package/src/lib/project/index.js +40 -0
  141. package/src/lib/project/paths.js +224 -0
  142. package/src/lib/project/version.js +110 -0
  143. package/src/lib/project/version.test.js +350 -0
  144. package/src/lib/reporter/README.md +211 -2
  145. package/src/lib/reporter/index.js +512 -0
  146. package/src/lib/reporter/index.test.js +593 -0
  147. package/src/main.js +134 -47
  148. package/src/main.spec.js +61 -0
  149. package/src/main.test.js +347 -0
  150. package/src/validators/utils.js +2 -1
  151. package/src/commands/conf.js +0 -43
  152. package/src/commands/version.js +0 -43
  153. package/src/lib/11ty/paths.js +0 -103
  154. package/src/lib/i18n/README.md +0 -3
  155. package/src/lib/i18n/config.js +0 -53
  156. package/src/lib/i18n/index.js +0 -43
  157. package/src/lib/i18n/localeService.js +0 -58
  158. package/src/lib/quire/README.md +0 -19
  159. package/src/lib/quire/index.js +0 -350
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Display formatting functions for Quire configuration
3
+ *
4
+ * Provides human-readable formatting with optional chalk styling.
5
+ * Separated from helpers.js to isolate the chalk dependency
6
+ * from the pure validation/coercion functions.
7
+ *
8
+ * @see ./helpers.js for schema-aware validation and coercion
9
+ */
10
+ import chalk from 'chalk'
11
+ import schema from './schema.js'
12
+
13
+ /**
14
+ * Get the description for a configuration key from the schema
15
+ *
16
+ * @param {string} key - Configuration key
17
+ * @returns {string|undefined} Description string, or undefined if not found
18
+ */
19
+ export function getKeyDescription(key) {
20
+ return schema[key]?.description
21
+ }
22
+
23
+ /**
24
+ * Format all settings as a human-readable string
25
+ *
26
+ * @param {Object} store - Configuration store (key-value pairs)
27
+ * @param {Object} [options]
28
+ * @param {string} [options.configPath] - Path to the config file (shown in header)
29
+ * @param {boolean} [options.showInternal=false] - Include __internal__ keys
30
+ * @param {boolean} [options.useColor=false] - Apply chalk styling to output
31
+ * @returns {string} Formatted multiline settings display
32
+ */
33
+ export function formatSettings(store, { configPath, showInternal = false, useColor = false } = {}) {
34
+ const style = useColor
35
+ ? { bold: chalk.bold, cyan: chalk.cyan, dim: chalk.dim }
36
+ : { bold: (s) => s, cyan: (s) => s, dim: (s) => s }
37
+
38
+ const lines = []
39
+
40
+ if (configPath) {
41
+ lines.push(style.bold(`quire-cli configuration ${configPath}`))
42
+ } else {
43
+ lines.push(style.bold('quire-cli configuration'))
44
+ }
45
+ lines.push('')
46
+
47
+ for (const [key, value] of Object.entries(store)) {
48
+ if (key.startsWith('__internal__') && !showInternal) continue
49
+ const description = getKeyDescription(key)
50
+ lines.push(` ${style.cyan(key)}: ${JSON.stringify(value)}`)
51
+ if (description) {
52
+ lines.push(` ${style.dim(description)}`)
53
+ }
54
+ }
55
+
56
+ lines.push('')
57
+ lines.push(style.dim('Use "quire settings set <key> <value>" to change a setting'))
58
+
59
+ return lines.join('\n')
60
+ }
@@ -0,0 +1,106 @@
1
+ import test from 'ava'
2
+ import chalk from 'chalk'
3
+ import {
4
+ getKeyDescription,
5
+ formatSettings,
6
+ } from './format.js'
7
+
8
+ // =============================================================================
9
+ // getKeyDescription
10
+ // =============================================================================
11
+
12
+ test('getKeyDescription returns description for known key', (t) => {
13
+ const desc = getKeyDescription('logLevel')
14
+ t.is(typeof desc, 'string')
15
+ t.true(desc.length > 0)
16
+ })
17
+
18
+ test('getKeyDescription returns undefined for unknown key', (t) => {
19
+ t.is(getKeyDescription('unknownKey'), undefined)
20
+ })
21
+
22
+ // =============================================================================
23
+ // formatSettings (plain, useColor=false)
24
+ // =============================================================================
25
+
26
+ test('formatSettings includes header with config path', (t) => {
27
+ const output = formatSettings({}, { configPath: '/mock/path' })
28
+ t.true(output.includes('quire-cli configuration /mock/path'))
29
+ })
30
+
31
+ test('formatSettings includes header without config path', (t) => {
32
+ const output = formatSettings({})
33
+ t.true(output.includes('quire-cli configuration'))
34
+ })
35
+
36
+ test('formatSettings includes key-value pairs', (t) => {
37
+ const store = { logLevel: 'info', logShowLevel: false }
38
+ const output = formatSettings(store)
39
+ t.true(output.includes('logLevel: "info"'))
40
+ t.true(output.includes('logShowLevel: false'))
41
+ })
42
+
43
+ test('formatSettings includes schema descriptions', (t) => {
44
+ const store = { logLevel: 'info' }
45
+ const output = formatSettings(store)
46
+ const desc = getKeyDescription('logLevel')
47
+ t.true(output.includes(desc))
48
+ })
49
+
50
+ test('formatSettings hides __internal__ keys by default', (t) => {
51
+ const store = { logLevel: 'info', __internal__secret: 'hidden' }
52
+ const output = formatSettings(store)
53
+ t.false(output.includes('__internal__secret'))
54
+ })
55
+
56
+ test('formatSettings shows __internal__ keys with showInternal', (t) => {
57
+ const store = { logLevel: 'info', __internal__secret: 'hidden' }
58
+ const output = formatSettings(store, { showInternal: true })
59
+ t.true(output.includes('__internal__secret'))
60
+ t.true(output.includes('"hidden"'))
61
+ })
62
+
63
+ test('formatSettings includes help hint', (t) => {
64
+ const output = formatSettings({})
65
+ t.true(output.includes('Use "quire settings set <key> <value>" to change a setting'))
66
+ })
67
+
68
+ // =============================================================================
69
+ // formatSettings (useColor=true)
70
+ // =============================================================================
71
+
72
+ test('formatSettings with useColor applies bold to header', (t) => {
73
+ const output = formatSettings({}, { useColor: true })
74
+ t.true(output.includes(chalk.bold('quire-cli configuration')))
75
+ })
76
+
77
+ test('formatSettings with useColor applies bold to header with path', (t) => {
78
+ const output = formatSettings({}, { useColor: true, configPath: '/mock/path' })
79
+ t.true(output.includes(chalk.bold('quire-cli configuration /mock/path')))
80
+ })
81
+
82
+ test('formatSettings with useColor applies cyan to key names', (t) => {
83
+ const store = { logLevel: 'info' }
84
+ const output = formatSettings(store, { useColor: true })
85
+ t.true(output.includes(chalk.cyan('logLevel')))
86
+ })
87
+
88
+ test('formatSettings with useColor applies dim to descriptions', (t) => {
89
+ const store = { logLevel: 'info' }
90
+ const output = formatSettings(store, { useColor: true })
91
+ const desc = getKeyDescription('logLevel')
92
+ t.true(output.includes(chalk.dim(desc)))
93
+ })
94
+
95
+ test('formatSettings with useColor applies dim to help hint', (t) => {
96
+ const output = formatSettings({}, { useColor: true })
97
+ t.true(output.includes(chalk.dim('Use "quire settings set <key> <value>" to change a setting')))
98
+ })
99
+
100
+ test('formatSettings without useColor does not apply ANSI codes', (t) => {
101
+ const store = { logLevel: 'info' }
102
+ const output = formatSettings(store)
103
+ // Plain output should not contain ANSI escape sequences
104
+ // eslint-disable-next-line no-control-regex
105
+ t.false(/\u001b\[/.test(output))
106
+ })
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Pure helper functions for Quire configuration
3
+ *
4
+ * Provides schema-aware validation and coercion
5
+ * without side effects or dependencies on the Conf instance.
6
+ *
7
+ * Nota bene: these functions import only schema and defaults (pure data),
8
+ * avoiding circular dependencies with the logger module.
9
+ *
10
+ * @see ./format.js for display formatting functions
11
+ */
12
+ import schema from './schema.js'
13
+ import defaults from './defaults.js'
14
+
15
+ /**
16
+ * Check whether a key is a valid configuration key
17
+ *
18
+ * @param {string} key - Configuration key to check
19
+ * @returns {boolean} True if the key exists in the schema
20
+ */
21
+ export function isValidKey(key) {
22
+ return Object.hasOwn(schema, key)
23
+ }
24
+
25
+ /**
26
+ * Get all valid configuration keys
27
+ *
28
+ * @returns {string[]} Sorted array of valid configuration keys
29
+ */
30
+ export function getValidKeys() {
31
+ return Object.keys(schema).sort()
32
+ }
33
+
34
+ /**
35
+ * Coerce a CLI string value to the appropriate type based on schema
36
+ *
37
+ * @param {string} key - Configuration key
38
+ * @param {string} value - String value from CLI input
39
+ * @returns {*} Coerced value matching the schema type
40
+ */
41
+ export function coerceValue(key, value) {
42
+ const keySchema = schema[key]
43
+ if (!keySchema) return value
44
+
45
+ switch (keySchema.type) {
46
+ case 'boolean':
47
+ if (value === 'true' || value === '1') return true
48
+ if (value === 'false' || value === '0') return false
49
+ return value
50
+ case 'number': {
51
+ const num = Number(value)
52
+ return isNaN(num) ? value : num
53
+ }
54
+ default:
55
+ return value
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Format a validation error with helpful information from the schema
61
+ *
62
+ * @param {string} key - Configuration key
63
+ * @param {*} value - The invalid value
64
+ * @returns {string} Formatted multiline error message
65
+ */
66
+ export function formatValidationError(key, value) {
67
+ const keySchema = schema[key]
68
+ const lines = [`Invalid value for '${key}': ${JSON.stringify(value)}`]
69
+
70
+ if (keySchema) {
71
+ if (keySchema.enum) {
72
+ lines.push(`Valid values: ${keySchema.enum.join(', ')}`)
73
+ }
74
+ if (keySchema.description) {
75
+ lines.push(`Description: ${keySchema.description}`)
76
+ }
77
+ }
78
+
79
+ return lines.join('\n')
80
+ }
81
+
82
+ /**
83
+ * Get the default value for a configuration key
84
+ *
85
+ * @param {string} key - Configuration key
86
+ * @returns {*} Default value, or undefined if key has no default
87
+ */
88
+ export function getDefault(key) {
89
+ return defaults[key]
90
+ }
91
+
@@ -0,0 +1,136 @@
1
+ import test from 'ava'
2
+ import {
3
+ isValidKey,
4
+ getValidKeys,
5
+ coerceValue,
6
+ formatValidationError,
7
+ getDefault,
8
+ } from './helpers.js'
9
+
10
+ // =============================================================================
11
+ // isValidKey
12
+ // =============================================================================
13
+
14
+ test('isValidKey returns true for a known schema key', (t) => {
15
+ t.true(isValidKey('logLevel'))
16
+ })
17
+
18
+ test('isValidKey returns true for all schema keys', (t) => {
19
+ const keys = getValidKeys()
20
+ for (const key of keys) {
21
+ t.true(isValidKey(key), `${key} should be valid`)
22
+ }
23
+ })
24
+
25
+ test('isValidKey returns false for an unknown key', (t) => {
26
+ t.false(isValidKey('nonExistentKey'))
27
+ })
28
+
29
+ test('isValidKey returns false for __internal__ prefixed keys', (t) => {
30
+ t.false(isValidKey('__internal__secretKey'))
31
+ })
32
+
33
+ // =============================================================================
34
+ // getValidKeys
35
+ // =============================================================================
36
+
37
+ test('getValidKeys returns an array', (t) => {
38
+ const keys = getValidKeys()
39
+ t.true(Array.isArray(keys))
40
+ t.true(keys.length > 0)
41
+ })
42
+
43
+ test('getValidKeys returns sorted keys', (t) => {
44
+ const keys = getValidKeys()
45
+ const sorted = [...keys].sort()
46
+ t.deepEqual(keys, sorted)
47
+ })
48
+
49
+ test('getValidKeys includes expected keys', (t) => {
50
+ const keys = getValidKeys()
51
+ t.true(keys.includes('logLevel'))
52
+ t.true(keys.includes('quireVersion'))
53
+ t.true(keys.includes('projectTemplate'))
54
+ })
55
+
56
+ // =============================================================================
57
+ // coerceValue
58
+ // =============================================================================
59
+
60
+ test('coerceValue coerces "true" to boolean true', (t) => {
61
+ t.is(coerceValue('logShowLevel', 'true'), true)
62
+ })
63
+
64
+ test('coerceValue coerces "false" to boolean false', (t) => {
65
+ t.is(coerceValue('logShowLevel', 'false'), false)
66
+ })
67
+
68
+ test('coerceValue coerces "1" to boolean true for boolean schema', (t) => {
69
+ t.is(coerceValue('logShowLevel', '1'), true)
70
+ })
71
+
72
+ test('coerceValue coerces "0" to boolean false for boolean schema', (t) => {
73
+ t.is(coerceValue('logShowLevel', '0'), false)
74
+ })
75
+
76
+ test('coerceValue passes through non-boolean string for boolean schema', (t) => {
77
+ t.is(coerceValue('logShowLevel', 'maybe'), 'maybe')
78
+ })
79
+
80
+ test('coerceValue passes through string values for string schema', (t) => {
81
+ t.is(coerceValue('logLevel', 'debug'), 'debug')
82
+ })
83
+
84
+ test('coerceValue passes through value for unknown key', (t) => {
85
+ t.is(coerceValue('unknownKey', 'someValue'), 'someValue')
86
+ })
87
+
88
+ // =============================================================================
89
+ // formatValidationError
90
+ // =============================================================================
91
+
92
+ test('formatValidationError includes invalid value', (t) => {
93
+ const error = formatValidationError('logLevel', 'invalid')
94
+ t.true(error.includes("Invalid value for 'logLevel'"))
95
+ t.true(error.includes('"invalid"'))
96
+ })
97
+
98
+ test('formatValidationError includes valid enum values', (t) => {
99
+ const error = formatValidationError('logLevel', 'invalid')
100
+ t.true(error.includes('Valid values:'))
101
+ t.true(error.includes('info'))
102
+ t.true(error.includes('debug'))
103
+ })
104
+
105
+ test('formatValidationError includes description', (t) => {
106
+ const error = formatValidationError('logLevel', 'invalid')
107
+ t.true(error.includes('Description:'))
108
+ })
109
+
110
+ test('formatValidationError handles key without enum', (t) => {
111
+ const error = formatValidationError('logPrefix', 123)
112
+ t.true(error.includes("Invalid value for 'logPrefix'"))
113
+ t.false(error.includes('Valid values:'))
114
+ })
115
+
116
+ test('formatValidationError handles unknown key', (t) => {
117
+ const error = formatValidationError('unknownKey', 'value')
118
+ t.true(error.includes("Invalid value for 'unknownKey'"))
119
+ })
120
+
121
+ // =============================================================================
122
+ // getDefault
123
+ // =============================================================================
124
+
125
+ test('getDefault returns default value for known key', (t) => {
126
+ t.is(getDefault('logLevel'), 'info')
127
+ })
128
+
129
+ test('getDefault returns default for boolean key', (t) => {
130
+ t.is(getDefault('logShowLevel'), false)
131
+ })
132
+
133
+ test('getDefault returns undefined for unknown key', (t) => {
134
+ t.is(getDefault('unknownKey'), undefined)
135
+ })
136
+
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Quire configuration module
3
+ *
4
+ * Exports the singleton Conf instance as the default export
5
+ * and named helper functions for schema-aware operations.
6
+ *
7
+ * @example
8
+ * import config from '#lib/conf/index.js'
9
+ * import { isValidKey, coerceValue } from '#lib/conf/index.js'
10
+ */
11
+ export { default as default } from './config.js'
12
+ export {
13
+ isValidKey,
14
+ getValidKeys,
15
+ coerceValue,
16
+ formatValidationError,
17
+ getDefault,
18
+ } from './helpers.js'
19
+ export {
20
+ getKeyDescription,
21
+ formatSettings,
22
+ } from './format.js'
@@ -2,29 +2,74 @@
2
2
  * Quire configuration schema
3
3
  * @see https://github.com/sindresorhus/conf#schema
4
4
  *
5
- * Nota bene: schema default values will are overwritten by `defaults.js` values
5
+ * Nota bene: default values here are overwritten by values in defaults.js
6
6
  * @see https://github.com/sindresorhus/conf#defaults
7
7
  */
8
+ import { schema as epubEngineSchema } from '#lib/epub/schema.js'
9
+ import { schema as pdfEngineSchema } from '#lib/pdf/schema.js'
10
+
8
11
  export default {
12
+ debug: {
13
+ type: 'boolean',
14
+ description: 'Enable debug output by default (equivalent to --debug flag)'
15
+ },
16
+ epubEngine: epubEngineSchema,
17
+ pdfEngine: pdfEngineSchema,
9
18
  logLevel: {
10
- type: 'string'
19
+ type: 'string',
20
+ enum: ['trace', 'debug', 'info', 'warn', 'error', 'silent'],
21
+ description: 'Minimum log level to display (trace, debug, info, warn, error, silent)'
22
+ },
23
+ logPrefix: {
24
+ type: 'string',
25
+ description: 'Prefix text shown before log messages'
26
+ },
27
+ logPrefixStyle: {
28
+ type: 'string',
29
+ enum: ['bracket', 'emoji', 'plain', 'none'],
30
+ description: 'Style for displaying the log prefix (bracket: [quire], emoji: 📖, plain: quire:, none: no prefix)'
31
+ },
32
+ logShowLevel: {
33
+ type: 'boolean',
34
+ description: 'Show log level label (INFO, WARN, etc.) in output'
35
+ },
36
+ logUseColor: {
37
+ type: 'boolean',
38
+ description: 'Use colored output for log messages'
39
+ },
40
+ logColorMessages: {
41
+ type: 'boolean',
42
+ description: 'Color message text by log level (e.g., red for errors). Requires logUseColor'
11
43
  },
12
44
  projectTemplate: {
13
- type: 'string'
45
+ type: 'string',
46
+ format: 'uri',
47
+ description: 'URL of the default project template for quire new'
14
48
  },
15
49
  quire11tyPath: {
16
- type: 'string'
50
+ type: 'string',
51
+ description: 'Path to the quire-11ty package (use "." for local development)'
17
52
  },
18
53
  quireVersion: {
19
- type: 'string'
54
+ type: 'string',
55
+ description: 'Version of quire-11ty to use ("latest" or specific version)'
20
56
  },
21
57
  updateChannel: {
22
- type: 'string'
58
+ type: 'string',
59
+ enum: ['stable', 'rc', 'beta', 'alpha'],
60
+ description: 'Release channel for updates (stable, rc, beta, alpha)'
23
61
  },
24
62
  updateInterval: {
25
- type: 'string'
63
+ type: 'string',
64
+ enum: ['DAILY', 'WEEKLY', 'MONTHLY', 'NEVER'],
65
+ description: 'How often to check for updates (DAILY, WEEKLY, MONTHLY, NEVER)'
66
+ },
67
+ verbose: {
68
+ type: 'boolean',
69
+ description: 'Enable verbose output by default (equivalent to --verbose flag)'
26
70
  },
27
71
  versionFile: {
28
- type: 'string'
72
+ type: 'string',
73
+ description: 'Filename used to identify Quire projects'
29
74
  }
30
75
  }
@@ -1,7 +1,138 @@
1
1
  ## CLI lib/EPUB Module
2
2
 
3
- This module implements a façade for EPUB e-book file format generation libries. The module exports a single method that accepts a `lib` option and delegates to a façade for the specified EPUB library.
3
+ This module provides an abstraction for EPUB e-book generation across Epub.js and Pandoc. The exported function accepts a `lib` option and dynamically loads the corresponding engine façade.
4
+
5
+ ### Architecture
6
+
7
+ ```
8
+ ┌───────────────────────────────────────────────────────────┐
9
+ │ lib/epub/index.js (Façade) │
10
+ │ • Resolve engine (epubjs/pandoc) │
11
+ │ • Check engine availability (fail-fast) │
12
+ │ • Resolve output path (CLI > default) │
13
+ │ • Own reporter lifecycle (start/succeed/fail) │
14
+ │ • Dynamic import of engine implementation │
15
+ └───────────────────────────┬───────────────────────────────┘
16
+ │
17
+ ┌─────────────┴─────────────────┐
18
+ │ lib/epub/engines.js │
19
+ │ • Central engine registry │
20
+ │ • Metadata (name, binary, │
21
+ │ toolInfo for errors) │
22
+ └─────────────┬─────────────────┘
23
+ │
24
+ ┌────────────────┴────────────────────┐
25
+ │ │
26
+ ┌──────────▼───────────┐ ┌───────────▼────────────┐
27
+ │ lib/epub/epub.js │ │ lib/epub/pandoc.js │
28
+ │ • Epub.js wrapper │ │ • Pandoc CLI wrapper │
29
+ │ • Load manifest │ │ • Filter XHTML files │
30
+ │ • Generate EPUB │ │ • Invoke pandoc │
31
+ │ • Write file │ │ • Write file │
32
+ └──────────────────────┘ └────────────────────────┘
33
+ ```
34
+
35
+ ### Usage
36
+
37
+ ```javascript
38
+ import generateEpub from '#lib/epub/index.js'
39
+
40
+ // Generate EPUB with Epub.js (default)
41
+ const output = await generateEpub({
42
+ lib: 'epubjs',
43
+ output: 'my-book.epub',
44
+ reporter,
45
+ })
46
+
47
+ // Generate EPUB with Pandoc
48
+ const output = await generateEpub({
49
+ lib: 'pandoc',
50
+ output: 'my-book.epub',
51
+ reporter,
52
+ })
53
+ ```
54
+
55
+ ### Options
56
+
57
+ | Option | Type | Default | Description |
58
+ |--------|------|---------|-------------|
59
+ | `lib` | string | `'epubjs'` | Engine to use: `'epubjs'` or `'pandoc'` |
60
+ | `output` | string | `'{lib}.epub'` | Output file path |
61
+ | `reporter` | object | required | Reporter instance for progress messages |
62
+ | `debug` | boolean | `false` | Enable debug output |
63
+
64
+ ### Output Path Resolution
65
+
66
+ The output path is resolved in priority order:
67
+ 1. CLI `--output` option value
68
+ 2. Default: `{lib}.epub` (e.g., `epubjs.epub` or `pandoc.epub`)
69
+
70
+ Relative paths are resolved against the project root. The `.epub` extension is auto-added if missing.
4
71
 
5
72
  ### Epub.js Façade
6
73
 
7
- See the [`Epub.js` documentation](https://github.com/futurepress/epub.js).
74
+ Wraps the [`epubjs-cli`](https://github.com/futurepress/epubjs-cli) package for EPUB generation.
75
+
76
+ **How it works:**
77
+ 1. Loads `manifest.json` from the build output directory
78
+ 2. Uses `ManifestToEpub` to generate the EPUB content
79
+ 3. Writes the generated file to the output path
80
+
81
+ See the [Epub.js documentation](https://github.com/futurepress/epub.js) for more details.
82
+
83
+ ### Pandoc Façade
84
+
85
+ Wraps the [Pandoc](https://pandoc.org/) command-line tool for EPUB generation.
86
+
87
+ **How it works:**
88
+ 1. Filters XHTML files from the build output
89
+ 2. Invokes `pandoc` with appropriate options
90
+ 3. Writes the generated EPUB to the output path
91
+
92
+ **Requirements:** Pandoc must be installed and available in PATH.
93
+
94
+ See the [Pandoc documentation](https://pandoc.org/MANUAL.html#creating-epubs-with-pandoc) for EPUB-specific options.
95
+
96
+ ### Engine Availability Check
97
+
98
+ The façade performs a fail-fast check before starting the reporter:
99
+
100
+ ```javascript
101
+ // Runs BEFORE reporter.start()
102
+ checkEngineAvailable(engine) // Throws ToolNotFoundError if binary missing
103
+ ```
104
+
105
+ - **Epub.js:** No binary check needed (pure JavaScript)
106
+ - **Pandoc:** Checks for `pandoc` in PATH
107
+
108
+ If the engine is unavailable, a `ToolNotFoundError` is thrown with:
109
+ - Install URL for the missing tool
110
+ - Fallback suggestion (use default engine)
111
+ - Link to documentation
112
+
113
+ ### Error Handling
114
+
115
+ The module uses typed errors for consistent error handling:
116
+
117
+ | Error | When Thrown |
118
+ |-------|-------------|
119
+ | `InvalidEpubLibraryError` | Unknown engine name provided |
120
+ | `EpubGenerationError` | Generation fails (with tool/operation context) |
121
+ | `ToolNotFoundError` | Required binary not found in PATH |
122
+
123
+ ### Test Strategy
124
+
125
+ | Component | Test Type | Coverage |
126
+ |-----------|-----------|----------|
127
+ | `commands/epub.js` | spec + integration | CLI interface, option handling |
128
+ | `lib/epub/index.js` | integration | Façade logic, path resolution, engine dispatch |
129
+ | `lib/epub/epub.js` | E2E | Happy path via full generation |
130
+ | `lib/epub/pandoc.js` | E2E | Happy path via full generation |
131
+
132
+ The engine implementations (`epub.js`, `pandoc.js`) are thin wrappers with limited logic, so E2E tests provide sufficient coverage. Error path tests would have lower ROI compared to the PDF module's `split.js`.
133
+
134
+ ### Related Documentation
135
+
136
+ - [EPUB Command](../../commands/epub.js) - CLI command implementation
137
+ - [Reporter Pattern](../reporter/README.md) - Progress reporting ownership
138
+ - [Error Classes](../../errors/output/) - Typed error definitions
@@ -0,0 +1,46 @@
1
+ /**
2
+ * EPUB Engine Registry
3
+ *
4
+ * Centralized metadata for EPUB engines. Each engine module re-exports
5
+ * its metadata from here, ensuring single source of truth.
6
+ *
7
+ * The façade imports this registry to:
8
+ * 1. Resolve engine names to implementations
9
+ * 2. Check binary availability before loading heavy modules
10
+ * 3. Provide helpful error messages with install URLs
11
+ */
12
+ import QuireError from '#src/errors/quire-error.js'
13
+
14
+ /**
15
+ * @typedef {Object} EngineMetadata
16
+ * @property {string} name - Human-readable display name
17
+ * @property {string} module - Module filename (e.g., 'epub.js')
18
+ * @property {string|null} requiresBinary - CLI binary name, or null if built-in
19
+ * @property {Object} [toolInfo] - Error metadata (only for engines requiring binaries)
20
+ * @property {string} [toolInfo.displayName] - Display name for error messages
21
+ * @property {string} [toolInfo.installUrl] - URL to download the tool
22
+ * @property {string} [toolInfo.docsUrl] - Quire docs URL
23
+ * @property {string} [toolInfo.fallback] - Alternative suggestion
24
+ */
25
+
26
+ /** @type {Record<string, EngineMetadata>} */
27
+ const ENGINES = {
28
+ epubjs: {
29
+ name: 'Epub.js',
30
+ module: 'epub.js',
31
+ requiresBinary: null, // Built-in Node.js library
32
+ },
33
+ pandoc: {
34
+ name: 'Pandoc',
35
+ module: 'pandoc.js',
36
+ requiresBinary: 'pandoc',
37
+ toolInfo: {
38
+ displayName: 'Pandoc',
39
+ installUrl: 'https://pandoc.org/installing.html',
40
+ docsUrl: `${QuireError.DOCS_BASE}/epub-output/`,
41
+ fallback: 'Or use the default EPUB engine: quire epub --engine epubjs',
42
+ },
43
+ },
44
+ }
45
+
46
+ export default ENGINES