@thegetty/quire-cli 1.0.0-rc.34 → 1.0.0-rc.36

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/schemas/config.schema.json +194 -0
  6. package/schemas/figures.schema.json +56 -0
  7. package/schemas/layout.schema.json +5 -0
  8. package/schemas/objects.schema.json +62 -0
  9. package/schemas/publication.schema.json +140 -0
  10. package/schemas/references.schema.json +29 -0
  11. package/src/Command.js +26 -6
  12. package/src/Command.spec.js +99 -0
  13. package/src/commands/README.md +213 -122
  14. package/src/commands/build.js +46 -37
  15. package/src/commands/build.spec.js +113 -0
  16. package/src/commands/build.test.js +402 -0
  17. package/src/commands/clean.js +25 -19
  18. package/src/commands/clean.spec.js +108 -0
  19. package/src/commands/clean.test.js +260 -0
  20. package/src/commands/config.js +251 -0
  21. package/src/commands/config.spec.js +107 -0
  22. package/src/commands/config.test.js +715 -0
  23. package/src/commands/create.js +42 -14
  24. package/src/commands/create.spec.js +112 -0
  25. package/src/commands/create.test.js +415 -0
  26. package/src/commands/epub.js +59 -30
  27. package/src/commands/epub.spec.js +114 -0
  28. package/src/commands/epub.test.js +503 -0
  29. package/src/commands/index.js +9 -2
  30. package/src/commands/info.js +18 -24
  31. package/src/commands/info.spec.js +64 -0
  32. package/src/commands/info.test.js +415 -0
  33. package/src/commands/pdf.js +58 -84
  34. package/src/commands/pdf.spec.js +114 -0
  35. package/src/commands/pdf.test.js +464 -0
  36. package/src/commands/preview.js +26 -31
  37. package/src/commands/preview.spec.js +97 -0
  38. package/src/commands/preview.test.js +250 -0
  39. package/src/commands/use.js +56 -0
  40. package/src/commands/use.spec.js +61 -0
  41. package/src/commands/use.test.js +280 -0
  42. package/src/commands/validate.js +31 -19
  43. package/src/commands/validate.spec.js +82 -0
  44. package/src/commands/validate.test.js +234 -0
  45. package/src/commands/workflows.js +70 -0
  46. package/src/errors/build/build-failed-error.js +19 -0
  47. package/src/errors/build/config-field-missing-error.js +20 -0
  48. package/src/errors/build/config-file-not-found-error.js +20 -0
  49. package/src/errors/build/index.js +11 -0
  50. package/src/errors/index.js +48 -0
  51. package/src/errors/install/dependency-install-error.js +19 -0
  52. package/src/errors/install/directory-not-empty-error.js +25 -0
  53. package/src/errors/install/index.js +12 -0
  54. package/src/errors/install/invalid-path-error.js +31 -0
  55. package/src/errors/install/invalid-starter-error.js +27 -0
  56. package/src/errors/install/version-not-found-error.js +35 -0
  57. package/src/errors/output/epub-generation-error.js +19 -0
  58. package/src/errors/output/index.js +14 -0
  59. package/src/errors/output/invalid-epub-library-error.js +20 -0
  60. package/src/errors/output/invalid-pdf-library-error.js +20 -0
  61. package/src/errors/output/missing-build-output-error.js +21 -0
  62. package/src/errors/output/pdf-generation-error.js +24 -0
  63. package/src/errors/output/tool-not-found-error.js +37 -0
  64. package/src/errors/project/index.js +10 -0
  65. package/src/errors/project/not-in-project-error.js +20 -0
  66. package/src/errors/project/project-create-error.js +21 -0
  67. package/src/errors/quire-error.js +27 -0
  68. package/src/errors/validation/validation-error.js +20 -11
  69. package/src/helpers/clean.js +1 -1
  70. package/src/helpers/docs-url.js +32 -0
  71. package/src/helpers/test-cwd.js +5 -6
  72. package/src/helpers/test-cwd.test.js +192 -0
  73. package/src/helpers/which.js +10 -4
  74. package/src/lib/11ty/README.md +135 -19
  75. package/src/lib/11ty/api.js +176 -93
  76. package/src/lib/11ty/cli.js +77 -35
  77. package/src/lib/11ty/index.js +64 -5
  78. package/src/lib/11ty/index.test.js +655 -0
  79. package/src/lib/README.md +275 -0
  80. package/src/lib/commander/index.js +100 -0
  81. package/src/lib/commander/index.test.js +86 -0
  82. package/src/lib/commander/options.js +195 -0
  83. package/src/lib/commander/options.test.js +109 -0
  84. package/src/lib/conf/README.md +84 -73
  85. package/src/lib/conf/config.js +5 -3
  86. package/src/lib/conf/config.test.js +281 -0
  87. package/src/lib/conf/defaults.js +44 -0
  88. package/src/lib/conf/format.js +60 -0
  89. package/src/lib/conf/format.test.js +106 -0
  90. package/src/lib/conf/helpers.js +91 -0
  91. package/src/lib/conf/helpers.test.js +136 -0
  92. package/src/lib/conf/index.js +22 -0
  93. package/src/lib/conf/schema.js +53 -8
  94. package/src/lib/epub/README.md +133 -2
  95. package/src/lib/epub/engines.js +46 -0
  96. package/src/lib/epub/epub.js +36 -11
  97. package/src/lib/epub/index.js +111 -21
  98. package/src/lib/epub/index.test.js +518 -0
  99. package/src/lib/epub/pandoc.js +33 -4
  100. package/src/lib/epub/pandoc.test.js +122 -0
  101. package/src/lib/epub/schema.js +21 -0
  102. package/src/lib/error/README.md +170 -0
  103. package/src/lib/error/handler.js +107 -0
  104. package/src/lib/git/README.md +151 -2
  105. package/src/lib/git/index.js +217 -13
  106. package/src/lib/git/index.spec.js +80 -0
  107. package/src/lib/git/index.test.js +453 -0
  108. package/src/lib/installer/index.js +309 -0
  109. package/src/lib/installer/index.spec.js +83 -0
  110. package/src/lib/installer/index.test.js +545 -0
  111. package/src/lib/logger/README.md +424 -0
  112. package/src/lib/logger/debug.js +90 -0
  113. package/src/lib/logger/debug.spec.js +130 -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,280 @@
1
+ import test from 'ava'
2
+ import { Volume, createFsFromVolume } from 'memfs'
3
+ import sinon from 'sinon'
4
+ import esmock from 'esmock'
5
+
6
+ test.beforeEach((t) => {
7
+ // Create sinon sandbox for mocking
8
+ t.context.sandbox = sinon.createSandbox()
9
+
10
+ // Create in-memory file system
11
+ t.context.vol = new Volume()
12
+ t.context.fs = createFsFromVolume(t.context.vol)
13
+
14
+ // Setup mock Quire project directory structure
15
+ t.context.vol.fromJSON({
16
+ '/project/.quire': '',
17
+ '/project/content/_data/config.yaml': 'title: Test Project',
18
+ '/project/package.json': JSON.stringify({ name: 'test-project' })
19
+ })
20
+
21
+ t.context.projectRoot = '/project'
22
+
23
+ // Create mock logger
24
+ t.context.mockLogger = {
25
+ info: t.context.sandbox.stub(),
26
+ error: t.context.sandbox.stub(),
27
+ debug: t.context.sandbox.stub(),
28
+ log: t.context.sandbox.stub(),
29
+ warn: t.context.sandbox.stub()
30
+ }
31
+
32
+ // Mock testcwd helper (returns true for Quire projects)
33
+ t.context.mockTestcwd = t.context.sandbox.stub()
34
+ })
35
+
36
+ test.afterEach.always((t) => {
37
+ // Restore all mocks
38
+ t.context.sandbox.restore()
39
+
40
+ // Clear in-memory file system
41
+ t.context.vol.reset()
42
+ })
43
+
44
+ test('use command should validate Quire project in preAction', async (t) => {
45
+ const { sandbox, mockLogger, mockTestcwd } = t.context
46
+
47
+ const { default: UseCommand } = await esmock('./use.js', {
48
+ '#helpers/test-cwd.js': {
49
+ default: mockTestcwd
50
+ }
51
+ }, {
52
+ '#lib/logger/index.js': {
53
+ default: () => mockLogger
54
+ }
55
+ })
56
+
57
+ const command = new UseCommand()
58
+ command.logger = mockLogger
59
+ command.debug = sandbox.stub()
60
+
61
+ // Mock Commander.js command object
62
+ const mockCommand = {
63
+ name: () => 'use',
64
+ opts: () => ({})
65
+ }
66
+
67
+ // Call preAction
68
+ command.preAction(mockCommand)
69
+
70
+ t.true(mockTestcwd.called, 'testcwd should be called in preAction')
71
+ t.true(mockTestcwd.calledWith(mockCommand), 'testcwd should be called with command')
72
+ })
73
+
74
+ test('use command should accept version argument', async (t) => {
75
+ const { sandbox, mockLogger, mockTestcwd } = t.context
76
+
77
+ const mockLatest = sandbox.stub().resolves('1.0.0-rc.33')
78
+ const mockSetVersion = sandbox.stub()
79
+
80
+ const { default: UseCommand } = await esmock('./use.js', {
81
+ '#helpers/test-cwd.js': {
82
+ default: mockTestcwd
83
+ },
84
+ '#lib/installer/index.js': {
85
+ latest: mockLatest
86
+ },
87
+ '#lib/project/index.js': {
88
+ setVersion: mockSetVersion
89
+ }
90
+ }, {
91
+ '#lib/logger/index.js': {
92
+ default: () => mockLogger
93
+ }
94
+ })
95
+
96
+ const command = new UseCommand()
97
+ command.logger = mockLogger
98
+ command.debug = sandbox.stub()
99
+
100
+ // Call action with version argument
101
+ await command.action('1.0.0-rc.33', {})
102
+
103
+ t.true(mockLatest.calledWith('1.0.0-rc.33'), 'latest should be called with version')
104
+ t.true(mockSetVersion.calledWith('1.0.0-rc.33'), 'setVersion should be called with resolved version')
105
+ })
106
+
107
+ test('use command should log debug information when debug flag is set', async (t) => {
108
+ const { sandbox, mockLogger, mockTestcwd } = t.context
109
+
110
+ const mockDebug = sandbox.stub()
111
+ const mockLatest = sandbox.stub().resolves('1.0.0-rc.33')
112
+ const mockSetVersion = sandbox.stub()
113
+
114
+ const { default: UseCommand } = await esmock('./use.js', {
115
+ '#helpers/test-cwd.js': {
116
+ default: mockTestcwd
117
+ },
118
+ '#lib/installer/index.js': {
119
+ latest: mockLatest
120
+ },
121
+ '#lib/project/index.js': {
122
+ setVersion: mockSetVersion
123
+ }
124
+ }, {
125
+ '#lib/logger/index.js': {
126
+ default: () => mockLogger
127
+ }
128
+ })
129
+
130
+ const command = new UseCommand()
131
+ command.logger = mockLogger
132
+ command.debug = mockDebug
133
+
134
+ // Call action with debug flag
135
+ await command.action('1.0.0-rc.33', { debug: true })
136
+
137
+ // Verify debug was called
138
+ t.true(mockDebug.called, 'debug should be called with debug flag')
139
+ t.true(mockDebug.calledWith('called with options %O', { debug: true }))
140
+ })
141
+
142
+ test('use command should handle semver version strings', async (t) => {
143
+ const { sandbox, mockLogger, mockTestcwd } = t.context
144
+
145
+ // Mock the installer's latest function to return the version
146
+ const mockLatest = sandbox.stub().callsFake(async (version) => version)
147
+ const mockSetVersion = sandbox.stub()
148
+
149
+ const { default: UseCommand } = await esmock('./use.js', {
150
+ '#helpers/test-cwd.js': {
151
+ default: mockTestcwd
152
+ },
153
+ '#lib/installer/index.js': {
154
+ latest: mockLatest
155
+ },
156
+ '#lib/project/index.js': {
157
+ setVersion: mockSetVersion
158
+ }
159
+ }, {
160
+ '#lib/logger/index.js': {
161
+ default: () => mockLogger
162
+ }
163
+ })
164
+
165
+ const command = new UseCommand()
166
+ command.logger = mockLogger
167
+ command.debug = sandbox.stub()
168
+
169
+ // Test various semver formats
170
+ const validVersions = [
171
+ '1.0.0',
172
+ '1.0.0-rc.1',
173
+ '1.0.0-beta.2',
174
+ '2.1.3',
175
+ '0.0.1'
176
+ ]
177
+
178
+ for (const version of validVersions) {
179
+ await command.action(version, {})
180
+ t.pass(`should accept valid semver: ${version}`)
181
+ }
182
+
183
+ t.is(mockLatest.callCount, validVersions.length, 'latest should be called for each version')
184
+ t.is(mockSetVersion.callCount, validVersions.length, 'setVersion should be called for each version')
185
+ })
186
+
187
+ test('use command should work with options object', async (t) => {
188
+ const { sandbox, mockLogger, mockTestcwd } = t.context
189
+
190
+ // Mock the installer's latest function to return the version
191
+ const mockLatest = sandbox.stub().callsFake(async (version) => version)
192
+ const mockSetVersion = sandbox.stub()
193
+
194
+ const { default: UseCommand } = await esmock('./use.js', {
195
+ '#helpers/test-cwd.js': {
196
+ default: mockTestcwd
197
+ },
198
+ '#lib/installer/index.js': {
199
+ latest: mockLatest
200
+ },
201
+ '#lib/project/index.js': {
202
+ setVersion: mockSetVersion
203
+ }
204
+ }, {
205
+ '#lib/logger/index.js': {
206
+ default: () => mockLogger
207
+ }
208
+ })
209
+
210
+ const command = new UseCommand()
211
+ command.logger = mockLogger
212
+ command.debug = sandbox.stub()
213
+
214
+ // Test with various options
215
+ await command.action('1.0.0', { debug: false })
216
+ await command.action('1.0.0', { debug: true })
217
+ await command.action('1.0.0', {})
218
+
219
+ t.is(mockLatest.callCount, 3, 'latest should be called for each action')
220
+ t.is(mockSetVersion.callCount, 3, 'setVersion should be called for each action')
221
+ t.pass('command should handle various option combinations')
222
+ })
223
+
224
+ test('use command preAction should be called before action', async (t) => {
225
+ const { sandbox, mockLogger, mockTestcwd } = t.context
226
+
227
+ const { default: UseCommand } = await esmock('./use.js', {
228
+ '#helpers/test-cwd.js': {
229
+ default: mockTestcwd
230
+ }
231
+ }, {
232
+ '#lib/logger/index.js': {
233
+ default: () => mockLogger
234
+ }
235
+ })
236
+
237
+ const command = new UseCommand()
238
+ command.logger = mockLogger
239
+ command.debug = sandbox.stub()
240
+
241
+ // Mock Commander.js command object
242
+ const mockCommand = {
243
+ name: () => 'use',
244
+ opts: () => ({})
245
+ }
246
+
247
+ // Verify preAction exists and can be called
248
+ t.is(typeof command.preAction, 'function', 'preAction should be a function')
249
+
250
+ // Call preAction (would be called by Commander.js before action)
251
+ command.preAction(mockCommand)
252
+
253
+ // Verify testcwd was called
254
+ t.true(mockTestcwd.called, 'testcwd should validate project directory')
255
+ })
256
+
257
+ test('use command should have correct command definition', async (t) => {
258
+ const { sandbox, mockLogger, mockTestcwd } = t.context
259
+
260
+ const { default: UseCommand } = await esmock('./use.js', {
261
+ '#helpers/test-cwd.js': {
262
+ default: mockTestcwd
263
+ }
264
+ }, {
265
+ '#lib/logger/index.js': {
266
+ default: () => mockLogger
267
+ }
268
+ })
269
+
270
+ const command = new UseCommand()
271
+ command.logger = mockLogger
272
+ command.debug = sandbox.stub()
273
+
274
+ t.is(command.name, 'use', 'command name should be "use"')
275
+ t.truthy(command.description, 'command should have description')
276
+ t.truthy(command.summary, 'command should have summary')
277
+ t.truthy(command.version, 'command should have version')
278
+ t.true(Array.isArray(command.args), 'command should have args array')
279
+ t.is(command.args.length, 1, 'command should have 1 argument')
280
+ })
@@ -1,47 +1,52 @@
1
1
  import Command from '#src/Command.js'
2
+ import { withOutputModes } from '#lib/commander/index.js'
2
3
  import { YAMLException } from 'js-yaml'
3
4
  import YamlValidationError from '../errors/validation/yaml-validation-error.js'
4
5
  import fs from 'fs-extra'
5
6
  import path from 'node:path'
6
- import { projectRoot } from '#lib/11ty/index.js'
7
+ import paths from '#lib/project/index.js'
7
8
  import testcwd from '../helpers/test-cwd.js'
8
9
  import yamlValidation from '../validators/validate-yaml.js'
9
-
10
+ import { ValidationError } from '#src/errors/index.js'
10
11
 
11
12
  /**
12
13
  * Quire CLI `validate` Command
13
- *
14
+ *
14
15
  * @class ValidateCommand
15
16
  * @extends {Command}
16
17
  */
17
18
  export default class ValidateCommand extends Command {
18
- static definition = {
19
+ static definition = withOutputModes({
19
20
  name: 'validate',
20
21
  description: 'Validate configuration files',
21
- summary: 'run validation',
22
+ summary: 'check YAML files for errors',
23
+ docsLink: 'quire-commands/#get-help',
24
+ helpText: `
25
+ Examples:
26
+ quire validate Check YAML syntax in content/_data/
27
+ quire validate --verbose Validate with detailed file listing
28
+
29
+ Validates YAML files in content/_data/ directory.
30
+ `,
22
31
  version: '1.0.0',
23
- options: [
24
- [ '--debug', 'run validate with debug output to console' ],
25
- ],
26
- }
32
+ options: [],
33
+ })
27
34
 
28
35
  constructor() {
29
36
  super(ValidateCommand.definition)
30
37
  }
31
38
 
32
39
  action(options, command){
33
- if(options.debug) {
34
- console.debug('[CLI] Command \'%s\' called with options %o', this.name(), options)
35
- }
36
-
37
- const dataPath = path.join(projectRoot, 'content', '_data')
40
+ this.debug('called with options %O', options)
41
+
42
+ const dataPath = path.join(paths.getProjectRoot(), 'content', '_data')
38
43
  const files = fs.readdirSync(dataPath)
39
44
  .filter(file => file.endsWith('.yaml') || file.endsWith('.yml'))
40
45
  .map(file => path.join(dataPath, file)
41
46
  )
42
47
 
43
48
  let errorList = []
44
- console.log('Validating YAML files..')
49
+ this.logger.info('Validating YAML files..')
45
50
 
46
51
  for (const file of files) {
47
52
  try {
@@ -52,13 +57,20 @@ export default class ValidateCommand extends Command {
52
57
  }
53
58
 
54
59
  if(errorList.length > 0) {
55
- errorList.forEach(err => { console.error(`${err.reason}`) })
60
+ errorList.forEach(err => { this.logger.error(`${err.reason}`) })
61
+ throw new ValidationError(
62
+ `Validation failed with ${errorList.length} error(s)`,
63
+ {
64
+ code: 'VALIDATION_FAILED',
65
+ suggestion: 'Fix the errors listed above and run validation again'
66
+ }
67
+ )
56
68
  } else {
57
- console.log('Validation complete.')
69
+ this.logger.info('Validation complete.')
58
70
  }
59
71
  }
60
72
 
61
- preAction(options, command) {
62
- testcwd(command)
73
+ preAction(thisCommand, actionCommand) {
74
+ testcwd(thisCommand)
63
75
  }
64
76
  }
@@ -0,0 +1,82 @@
1
+ import { Command, Option } from 'commander'
2
+ import program from '#src/main.js'
3
+ import test from 'ava'
4
+
5
+ /**
6
+ * Command Contract/Interface Tests
7
+ *
8
+ * Verifies the command's public API and Commander.js integration.
9
+ * @see docs/testing-commands.md
10
+ */
11
+
12
+ test.before((t) => {
13
+ // Get the registered command (from program.commands) once and share across all tests
14
+ t.context.command = program.commands.find((cmd) => cmd.name() === 'validate')
15
+ })
16
+
17
+ test('command is registered in CLI program', (t) => {
18
+ const { command } = t.context
19
+
20
+ t.truthy(command, 'command "validate" should be registered in program')
21
+ t.true(command instanceof Command, 'registered command should be Commander.js Command instance')
22
+ })
23
+
24
+ test('registered command has correct metadata', (t) => {
25
+ const { command } = t.context
26
+
27
+ t.is(command.name(), 'validate')
28
+ t.truthy(command.description())
29
+ t.is(typeof command._actionHandler, 'function', 'command should have action handler')
30
+ })
31
+
32
+ test('registered command has no arguments', (t) => {
33
+ const { command } = t.context
34
+ const registeredArguments = command.registeredArguments
35
+
36
+ t.is(registeredArguments.length, 0, 'validate command should have no arguments')
37
+ })
38
+
39
+ test('registered command has correct options', (t) => {
40
+ const { command } = t.context
41
+
42
+ // Get all options
43
+ const quietOption = command.options.find((opt) => opt.long === '--quiet')
44
+ const verboseOption = command.options.find((opt) => opt.long === '--verbose')
45
+ const debugOption = command.options.find((opt) => opt.long === '--debug')
46
+
47
+ // Verify all options exist
48
+ t.truthy(quietOption, '--quiet option should exist')
49
+ t.truthy(verboseOption, '--verbose option should exist')
50
+ t.truthy(debugOption, '--debug option should exist')
51
+
52
+ // Verify they are Option instances
53
+ t.true(quietOption instanceof Option, '--quiet should be Option instance')
54
+ t.true(verboseOption instanceof Option, '--verbose should be Option instance')
55
+ t.true(debugOption instanceof Option, '--debug should be Option instance')
56
+
57
+ // Verify option properties
58
+ t.is(quietOption.long, '--quiet')
59
+ t.is(quietOption.short, '-q')
60
+ t.truthy(quietOption.description)
61
+ t.false(quietOption.required, '--quiet should not require a value')
62
+
63
+ t.is(verboseOption.long, '--verbose')
64
+ t.is(verboseOption.short, '-v')
65
+ t.truthy(verboseOption.description)
66
+ t.false(verboseOption.required, '--verbose should not require a value')
67
+
68
+ t.is(debugOption.long, '--debug')
69
+ t.truthy(debugOption.description)
70
+ t.false(debugOption.required, '--debug should not require a value')
71
+ })
72
+
73
+ test('command options are accessible via public API', (t) => {
74
+ const { command } = t.context
75
+
76
+ // Test that options can be accessed the way Commander.js does
77
+ const optionNames = command.options.map((opt) => opt.long)
78
+
79
+ t.true(optionNames.includes('--quiet'))
80
+ t.true(optionNames.includes('--verbose'))
81
+ t.true(optionNames.includes('--debug'))
82
+ })
@@ -0,0 +1,234 @@
1
+ import test from 'ava'
2
+ import { Volume, createFsFromVolume } from 'memfs'
3
+ import sinon from 'sinon'
4
+ import esmock from 'esmock'
5
+ import { ValidationError } from '#src/errors/index.js'
6
+
7
+ test.beforeEach((t) => {
8
+ // Create sinon sandbox for mocking
9
+ t.context.sandbox = sinon.createSandbox()
10
+
11
+ // Create in-memory file system
12
+ t.context.vol = new Volume()
13
+ t.context.fs = createFsFromVolume(t.context.vol)
14
+
15
+ // Setup mock directory structure
16
+ t.context.vol.fromJSON({
17
+ '/project/content/_data/config.yaml': 'title: Quire Test Project\nidentifier:\n isbn: "978-0-12345-678-9"',
18
+ '/project/content/_data/publication.yaml': 'title: Test Publication',
19
+ '/project/package.json': JSON.stringify({ name: 'test-project' })
20
+ })
21
+
22
+ t.context.projectRoot = '/project'
23
+
24
+ // Stub console methods to suppress output during tests
25
+ // Create mock logger (no global console stubbing needed!)
26
+ t.context.mockLogger = {
27
+ info: t.context.sandbox.stub(),
28
+ error: t.context.sandbox.stub(),
29
+ debug: t.context.sandbox.stub(),
30
+ log: t.context.sandbox.stub(),
31
+ warn: t.context.sandbox.stub()
32
+ }
33
+ // mockLogger already created above
34
+ // mockLogger already created above
35
+ })
36
+
37
+ test.afterEach.always((t) => {
38
+ // Restore all mocks
39
+ t.context.sandbox.restore()
40
+
41
+ // Clear in-memory file system
42
+ t.context.vol.reset()
43
+ })
44
+
45
+ test('validate command should find and validate YAML files', async (t) => {
46
+ const { sandbox, fs, mockLogger } = t.context
47
+
48
+ // Mock yamlValidation function
49
+ const mockYamlValidation = sandbox.stub()
50
+
51
+ // Mock testcwd helper
52
+ const mockTestcwd = sandbox.stub()
53
+
54
+ // Use esmock to replace imports
55
+ const ValidateCommand = await esmock('./validate.js', {
56
+ '../validators/validate-yaml.js': {
57
+ default: mockYamlValidation
58
+ },
59
+ '#lib/project/index.js': {
60
+ default: { getProjectRoot: () => '/project' }
61
+ },
62
+ '#helpers/test-cwd.js': {
63
+ default: mockTestcwd
64
+ },
65
+ '#lib/logger/index.js': {
66
+ logger: mockLogger
67
+ },
68
+ 'fs-extra': fs
69
+ })
70
+
71
+ const command = new ValidateCommand()
72
+ command.name = sandbox.stub().returns('validate')
73
+
74
+ // Run action
75
+ command.action({}, command)
76
+
77
+ // Should validate both YAML files
78
+ t.true(mockYamlValidation.callCount >= 2, 'should validate multiple YAML files')
79
+ })
80
+
81
+ test('validate command should handle validation errors', async (t) => {
82
+ const { sandbox, fs, mockLogger } = t.context
83
+
84
+ // Mock yamlValidation function to throw error
85
+ const validationError = new Error('Invalid YAML')
86
+ validationError.reason = 'Invalid YAML: missing required field'
87
+ const mockYamlValidation = sandbox.stub().throws(validationError)
88
+
89
+ // Mock testcwd helper
90
+ const mockTestcwd = sandbox.stub()
91
+
92
+ // Use esmock to replace imports
93
+ const ValidateCommand = await esmock('./validate.js', {
94
+ '../validators/validate-yaml.js': {
95
+ default: mockYamlValidation
96
+ },
97
+ '#lib/project/index.js': {
98
+ default: { getProjectRoot: () => '/project' }
99
+ },
100
+ '#helpers/test-cwd.js': {
101
+ default: mockTestcwd
102
+ },
103
+ '#lib/logger/index.js': {
104
+ logger: mockLogger
105
+ },
106
+ 'fs-extra': fs
107
+ })
108
+
109
+ const command = new ValidateCommand()
110
+ command.name = sandbox.stub().returns('validate')
111
+ command.logger = mockLogger
112
+
113
+ // Run action - should throw ValidationError when there are validation errors
114
+ const error = t.throws(() => command.action({}, command), { instanceOf: ValidationError })
115
+
116
+ t.is(error.code, 'VALIDATION_FAILED', 'error should have correct code')
117
+ t.is(error.exitCode, 4, 'error should have validation exit code')
118
+ t.true(mockYamlValidation.called, 'validation should be attempted')
119
+ t.true(mockLogger.error.called, 'errors should be logged')
120
+ })
121
+
122
+ test('validate command should call testcwd in preAction', async (t) => {
123
+ const { sandbox, fs, mockLogger } = t.context
124
+
125
+ // Mock yamlValidation function
126
+ const mockYamlValidation = sandbox.stub()
127
+
128
+ // Mock testcwd helper
129
+ const mockTestcwd = sandbox.stub()
130
+
131
+ // Use esmock to replace imports
132
+ const ValidateCommand = await esmock('./validate.js', {
133
+ '../validators/validate-yaml.js': {
134
+ default: mockYamlValidation
135
+ },
136
+ '#lib/project/index.js': {
137
+ default: { getProjectRoot: () => '/project' }
138
+ },
139
+ '#helpers/test-cwd.js': {
140
+ default: mockTestcwd
141
+ },
142
+ '#lib/logger/index.js': {
143
+ logger: mockLogger
144
+ },
145
+ 'fs-extra': fs
146
+ })
147
+
148
+ const command = new ValidateCommand()
149
+ command.name = sandbox.stub().returns('validate')
150
+
151
+ // Run preAction
152
+ command.preAction({}, command)
153
+
154
+ t.true(mockTestcwd.called, 'testcwd should be called in preAction')
155
+ })
156
+
157
+ test('validate command should filter YAML files correctly', async (t) => {
158
+ const { sandbox, fs, vol, mockLogger } = t.context
159
+
160
+ // Add non-YAML files
161
+ vol.fromJSON({
162
+ '/project/content/_data/data.json': '{"key": "value"}',
163
+ '/project/content/_data/README.md': '# README',
164
+ '/project/content/_data/script.js': 'console.log("test")'
165
+ })
166
+
167
+ // Mock yamlValidation function
168
+ const mockYamlValidation = sandbox.stub()
169
+
170
+ // Mock testcwd helper
171
+ const mockTestcwd = sandbox.stub()
172
+
173
+ // Use esmock to replace imports
174
+ const ValidateCommand = await esmock('./validate.js', {
175
+ '../validators/validate-yaml.js': {
176
+ default: mockYamlValidation
177
+ },
178
+ '#lib/project/index.js': {
179
+ default: { getProjectRoot: () => '/project' }
180
+ },
181
+ '#helpers/test-cwd.js': {
182
+ default: mockTestcwd
183
+ },
184
+ '#lib/logger/index.js': {
185
+ logger: mockLogger
186
+ },
187
+ 'fs-extra': fs
188
+ })
189
+
190
+ const command = new ValidateCommand()
191
+ command.name = sandbox.stub().returns('validate')
192
+
193
+ // Run action
194
+ command.action({}, command)
195
+
196
+ // Should only validate YAML files (config.yaml and publication.yaml from beforeEach)
197
+ t.is(mockYamlValidation.callCount, 2, 'should only validate YAML files')
198
+ })
199
+
200
+ test('validate command should pass debug option through', async (t) => {
201
+ const { sandbox, fs, mockLogger } = t.context
202
+
203
+ // Mock yamlValidation function
204
+ const mockYamlValidation = sandbox.stub()
205
+
206
+ // Mock testcwd helper
207
+ const mockTestcwd = sandbox.stub()
208
+
209
+ // Use esmock to replace imports
210
+ const ValidateCommand = await esmock('./validate.js', {
211
+ '../validators/validate-yaml.js': {
212
+ default: mockYamlValidation
213
+ },
214
+ '#lib/project/index.js': {
215
+ default: { getProjectRoot: () => '/project' }
216
+ },
217
+ '#helpers/test-cwd.js': {
218
+ default: mockTestcwd
219
+ },
220
+ '#lib/logger/index.js': {
221
+ logger: mockLogger
222
+ },
223
+ 'fs-extra': fs
224
+ })
225
+
226
+ const command = new ValidateCommand()
227
+ command.name = sandbox.stub().returns('validate')
228
+
229
+ // Run action with debug option
230
+ command.action({ debug: true }, command)
231
+
232
+ t.true(mockYamlValidation.called, 'validation should run')
233
+ // Debug output should be logged (verified through console.debug stub)
234
+ })