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

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 (85) hide show
  1. package/package.json +3 -2
  2. package/src/commands/build.js +13 -6
  3. package/src/commands/clean.js +10 -2
  4. package/src/commands/clean.spec.js +10 -0
  5. package/src/commands/clean.test.js +122 -0
  6. package/src/commands/config.js +3 -1
  7. package/src/commands/config.test.js +42 -1
  8. package/src/commands/doctor.js +251 -0
  9. package/src/commands/doctor.spec.js +115 -0
  10. package/src/commands/doctor.test.js +1409 -0
  11. package/src/commands/epub.js +9 -2
  12. package/src/commands/info.js +79 -71
  13. package/src/commands/info.spec.js +8 -0
  14. package/src/commands/info.test.js +173 -76
  15. package/src/commands/pdf.js +10 -3
  16. package/src/commands/validate.js +39 -7
  17. package/src/commands/validate.spec.js +8 -0
  18. package/src/commands/validate.test.js +144 -0
  19. package/src/lib/README.md +77 -22
  20. package/src/lib/conf/README.md +6 -0
  21. package/src/lib/conf/build-status.js +103 -0
  22. package/src/lib/conf/build-status.test.js +247 -0
  23. package/src/lib/conf/defaults.js +14 -0
  24. package/src/lib/conf/format.js +1 -1
  25. package/src/lib/conf/schema.js +46 -1
  26. package/src/lib/constants.js +28 -0
  27. package/src/lib/doctor/README.md +667 -0
  28. package/src/lib/doctor/checks/environment/cli-version.js +62 -0
  29. package/src/lib/doctor/checks/environment/cli-version.test.js +132 -0
  30. package/src/lib/doctor/checks/environment/git-available.js +77 -0
  31. package/src/lib/doctor/checks/environment/git-available.test.js +52 -0
  32. package/src/lib/doctor/checks/environment/index.js +13 -0
  33. package/src/lib/doctor/checks/environment/node-version.js +66 -0
  34. package/src/lib/doctor/checks/environment/node-version.test.js +17 -0
  35. package/src/lib/doctor/checks/environment/npm-available.js +66 -0
  36. package/src/lib/doctor/checks/environment/npm-available.test.js +52 -0
  37. package/src/lib/doctor/checks/environment/os-info.js +48 -0
  38. package/src/lib/doctor/checks/environment/os-info.test.js +81 -0
  39. package/src/lib/doctor/checks/environment/runtime-info.js +51 -0
  40. package/src/lib/doctor/checks/environment/runtime-info.test.js +132 -0
  41. package/src/lib/doctor/checks/outputs/epub-output.js +119 -0
  42. package/src/lib/doctor/checks/outputs/epub-output.test.js +277 -0
  43. package/src/lib/doctor/checks/outputs/index.js +10 -0
  44. package/src/lib/doctor/checks/outputs/pdf-output.js +144 -0
  45. package/src/lib/doctor/checks/outputs/pdf-output.test.js +377 -0
  46. package/src/lib/doctor/checks/outputs/stale-build.js +122 -0
  47. package/src/lib/doctor/checks/outputs/stale-build.test.js +282 -0
  48. package/src/lib/doctor/checks/project/data-files.js +56 -0
  49. package/src/lib/doctor/checks/project/data-files.test.js +125 -0
  50. package/src/lib/doctor/checks/project/dependencies.js +53 -0
  51. package/src/lib/doctor/checks/project/dependencies.test.js +71 -0
  52. package/src/lib/doctor/checks/project/index.js +11 -0
  53. package/src/lib/doctor/checks/project/quire-11ty.js +98 -0
  54. package/src/lib/doctor/checks/project/quire-11ty.test.js +170 -0
  55. package/src/lib/doctor/checks/project/quire-project.js +38 -0
  56. package/src/lib/doctor/checks/project/quire-project.test.js +47 -0
  57. package/src/lib/doctor/checks/tools/index.js +10 -0
  58. package/src/lib/doctor/checks/tools/pandoc-available.js +82 -0
  59. package/src/lib/doctor/checks/tools/pandoc-available.test.js +73 -0
  60. package/src/lib/doctor/checks/tools/prince-available.js +81 -0
  61. package/src/lib/doctor/checks/tools/prince-available.test.js +73 -0
  62. package/src/lib/doctor/constants.js +39 -0
  63. package/src/lib/doctor/formatDuration.js +108 -0
  64. package/src/lib/doctor/formatDuration.test.js +76 -0
  65. package/src/lib/doctor/formatters/human.js +257 -0
  66. package/src/lib/doctor/formatters/human.test.js +463 -0
  67. package/src/lib/doctor/formatters/index.js +8 -0
  68. package/src/lib/doctor/formatters/json.js +78 -0
  69. package/src/lib/doctor/formatters/json.test.js +174 -0
  70. package/src/lib/doctor/formatters/shared.js +129 -0
  71. package/src/lib/doctor/formatters/shared.test.js +194 -0
  72. package/src/lib/doctor/index.js +271 -0
  73. package/src/lib/doctor/index.test.js +797 -0
  74. package/src/lib/platform.js +95 -0
  75. package/src/lib/project/build.js +44 -25
  76. package/src/lib/project/build.test.js +30 -8
  77. package/src/lib/project/detect.js +16 -2
  78. package/src/lib/project/index.js +18 -2
  79. package/src/lib/project/output-paths.js +87 -0
  80. package/src/lib/project/output-paths.test.js +66 -0
  81. package/src/lib/project/paths.js +48 -0
  82. package/src/main.js +24 -2
  83. package/src/packageConfig.js +17 -0
  84. package/src/validators/validate-data-files.js +154 -0
  85. package/src/validators/validate-data-files.test.js +217 -0
@@ -5,6 +5,7 @@ import eleventy from '#lib/11ty/index.js'
5
5
  import fs from 'fs-extra'
6
6
  import generateEpub, { ENGINES } from '#lib/epub/index.js'
7
7
  import open from 'open'
8
+ import { recordStatus } from '#lib/conf/build-status.js'
8
9
  import reporter from '#lib/reporter/index.js'
9
10
  import testcwd from '#helpers/test-cwd.js'
10
11
  import { MissingBuildOutputError } from '#src/errors/index.js'
@@ -84,9 +85,15 @@ Examples:
84
85
  // Pass engine as lib (matching lib/pdf interface)
85
86
  // Reporter lifecycle is owned by the façade (lib/epub/index.js)
86
87
  const epubOptions = { ...options, lib: options.engine }
87
- const output = await generateEpub(epubOptions)
88
88
 
89
- if (fs.existsSync(output) && options.open) open(output)
89
+ try {
90
+ const output = await generateEpub(epubOptions)
91
+ recordStatus(paths.getProjectRoot(), 'epub', 'ok')
92
+ if (fs.existsSync(output) && options.open) open(output)
93
+ } catch (error) {
94
+ recordStatus(paths.getProjectRoot(), 'epub', 'failed')
95
+ throw error
96
+ }
90
97
  }
91
98
 
92
99
  preAction(thisCommand, actionCommand) {
@@ -1,34 +1,52 @@
1
1
  import Command from '#src/Command.js'
2
- import npm from '#lib/npm/index.js'
3
- import { execa } from 'execa'
2
+ import { withOutputModes } from '#lib/commander/index.js'
3
+ import { binPath } from '#src/packageConfig.js'
4
4
  import fs from 'node:fs'
5
- import os from 'node:os'
6
5
  import path from 'node:path'
7
6
  import testcwd from '#helpers/test-cwd.js'
8
7
 
9
8
  /**
10
9
  * Quire CLI `info` Command
11
10
  *
12
- * Runs the Eleventy `info` command to list the quire-cli, quire-11ty, node, and npm versions.
11
+ * Display version information for the current Quire project.
12
+ * Shows the versions of quire-cli and quire-11ty that the project was
13
+ * created with, plus the starter template version.
14
+ *
15
+ * With --debug, also shows the full filesystem path to the quire-cli
16
+ * executable.
17
+ *
18
+ * For system environment information (OS, Node.js, npm, Git),
19
+ * use the `quire doctor` command instead.
13
20
  *
14
21
  * @class InfoCommand
15
22
  * @extends {Command}
16
23
  */
17
24
  export default class InfoCommand extends Command {
18
- static definition = {
25
+ static definition = withOutputModes({
19
26
  name: 'info',
20
- description: 'List Quire cli, quire-11ty, and node versions',
21
- summary: 'show version information',
27
+ description: 'Display version information for the current project',
28
+ summary: 'show project version information',
22
29
  docsLink: 'quire-commands/#get-help',
23
30
  helpText: `
31
+ Shows the versions used when this project was created:
32
+ • quire-cli version that created the project
33
+ • quire-11ty version installed in the project
34
+ • starter template version (if available)
35
+
36
+ For system environment checks (OS, Node.js, npm, Git),
37
+ use 'quire doctor' instead.
38
+
24
39
  Example:
25
- quire info --debug Include node, npm, and OS versions
40
+ quire info Show project versions
41
+ quire info --debug Include installation paths
42
+ quire info --json Output version information as JSON
43
+ quire doctor Check environment and project health
26
44
  `,
27
45
  version: '1.0.0',
28
46
  options: [
29
- ['--debug', 'include os versions in output']
47
+ ['--json', 'output version information as JSON'],
30
48
  ],
31
- }
49
+ })
32
50
 
33
51
  constructor() {
34
52
  super(InfoCommand.definition)
@@ -43,79 +61,69 @@ Example:
43
61
 
44
62
  try {
45
63
  const versionFileData = fs.readFileSync(versionFileName, { encoding: 'utf8' })
46
-
47
- versionInfo = JSON.parse(versionFileData)
64
+ versionInfo = JSON.parse(versionFileData)
48
65
  } catch (error) {
49
66
  this.logger.warn(
50
67
  `This project was generated with the quire-cli prior to version 1.0.0.rc-8. Updating the version file to the new format, though this project's version file will not contain specific starter version information.`
51
68
  )
52
-
53
69
  fs.writeFileSync(versionFileName, JSON.stringify(versionInfo))
54
70
  }
55
71
 
56
72
  const { name: projectDirectory } = path.parse(process.cwd())
57
73
 
58
- const versions = [
59
- {
60
- title: `[${projectDirectory}]`,
61
- items: [
62
- {
63
- name: 'quire-cli',
64
- get: () => versionInfo.cli,
65
- },
66
- {
67
- name: 'quire-11ty',
68
- get: () => {
69
- const { version } = JSON.parse(fs.readFileSync('./package.json'))
70
- return version
71
- },
72
- },
73
- {
74
- name: 'starter',
75
- get: () => versionInfo.starter,
76
- },
77
- ],
78
- },
79
- {
80
- title: '[System]',
81
- items: [
82
- {
83
- name: 'quire-cli',
84
- get: async () => {
85
- const { stdout } = await execa('quire', ['--version'])
86
- return stdout
87
- },
88
- },
89
- {
90
- debug: true,
91
- name: 'node',
92
- get: () => process.version,
93
- },
94
- {
95
- debug: true,
96
- name: 'npm',
97
- get: async () => await npm.version(),
98
- },
99
- {
100
- debug: true,
101
- name: 'os',
102
- get: () => `${os.type()} ${os.release()}`,
103
- },
104
- ],
105
- },
74
+ // Read quire-11ty version from project's package.json
75
+ let quire11tyVersion = 'unknown'
76
+ try {
77
+ const packageJson = JSON.parse(fs.readFileSync('./package.json', 'utf8'))
78
+ quire11tyVersion = packageJson.version
79
+ } catch {
80
+ this.debug('Could not read package.json')
81
+ }
82
+
83
+ if (options.json) {
84
+ const result = {
85
+ project: {
86
+ directory: projectDirectory,
87
+ cli: versionInfo.cli,
88
+ quire11ty: quire11tyVersion,
89
+ starter: versionInfo.starter || null,
90
+ },
91
+ }
92
+ console.log(JSON.stringify(result, null, 2))
93
+ return
94
+ }
95
+
96
+ const lines = [
97
+ `Project: ${projectDirectory}`,
98
+ '',
99
+ ` quire-cli ${versionInfo.cli || 'unknown'}`,
100
+ ` quire-11ty ${quire11tyVersion}`,
106
101
  ]
107
102
 
108
- /**
109
- * Filter the command output based on `debug` settings
110
- */
111
- for (const { items, title } of versions) {
112
- const versionList = await Promise.all(
113
- items
114
- .filter(({ debug }) => !debug || (options.debug && debug))
115
- .map(async ({ name, get }) => `${name} ${await get()}`)
116
- )
117
- this.logger.info(`${title}\n ${versionList.join('\n ')}`)
103
+ if (versionInfo.starter) {
104
+ lines.push(` starter ${versionInfo.starter}`)
105
+ }
106
+
107
+ if (options.debug) {
108
+ lines.push('')
109
+ lines.push(this.resolveCliPath())
118
110
  }
111
+
112
+ lines.push('')
113
+ lines.push('Tip: Run \'quire doctor\' for system environment checks')
114
+
115
+ this.logger.info(lines.join('\n'))
116
+ }
117
+
118
+ /**
119
+ * Resolve the full filesystem path to the quire CLI executable
120
+ * @returns {string} Formatted path line
121
+ */
122
+ resolveCliPath() {
123
+ const resolved = binPath()
124
+ return resolved
125
+ ? ` quire-cli ${resolved}`
126
+ : ' quire-cli not found in PATH'
119
127
  }
120
128
 
121
129
  preAction(thisCommand, actionCommand) {
@@ -40,15 +40,22 @@ test('registered command has correct options', (t) => {
40
40
  const { command } = t.context
41
41
 
42
42
  // Get all options
43
+ const jsonOption = command.options.find((opt) => opt.long === '--json')
43
44
  const debugOption = command.options.find((opt) => opt.long === '--debug')
44
45
 
45
46
  // Verify all options exist
47
+ t.truthy(jsonOption, '--json option should exist')
46
48
  t.truthy(debugOption, '--debug option should exist')
47
49
 
48
50
  // Verify they are Option instances
51
+ t.true(jsonOption instanceof Option, '--json should be Option instance')
49
52
  t.true(debugOption instanceof Option, '--debug should be Option instance')
50
53
 
51
54
  // Verify option properties
55
+ t.is(jsonOption.long, '--json')
56
+ t.truthy(jsonOption.description)
57
+ t.false(jsonOption.required, '--json should not require a value')
58
+
52
59
  t.is(debugOption.long, '--debug')
53
60
  t.truthy(debugOption.description)
54
61
  t.false(debugOption.required, '--debug should not require a value')
@@ -60,5 +67,6 @@ test('command options are accessible via public API', (t) => {
60
67
  // Test that options can be accessed the way Commander.js does
61
68
  const optionNames = command.options.map((opt) => opt.long)
62
69
 
70
+ t.true(optionNames.includes('--json'))
63
71
  t.true(optionNames.includes('--debug'))
64
72
  })
@@ -23,16 +23,8 @@ test.beforeEach((t) => {
23
23
  // Mock testcwd helper
24
24
  t.context.mockTestcwd = t.context.sandbox.stub()
25
25
 
26
- // Mock execaCommand for external commands
27
- t.context.mockExecaCommand = t.context.sandbox.stub()
28
- t.context.mockExecaCommand.withArgs('quire --version').resolves({ stdout: '1.0.0-rc.33' })
29
- t.context.mockExecaCommand.withArgs('npm --version').resolves({ stdout: '10.2.4' })
30
-
31
- // Mock os module
32
- t.context.mockOs = {
33
- type: t.context.sandbox.stub().returns('Darwin'),
34
- release: t.context.sandbox.stub().returns('23.0.0')
35
- }
26
+ // Mock packageConfig binPath
27
+ t.context.mockBinPath = t.context.sandbox.stub().returns('/usr/local/bin/quire')
36
28
 
37
29
  // Mock fs module
38
30
  t.context.mockFs = {
@@ -72,16 +64,15 @@ test.afterEach.always((t) => {
72
64
  })
73
65
 
74
66
  test('info command should validate Quire project in preAction', async (t) => {
75
- const { sandbox, mockLogger, mockTestcwd, mockConfig, mockExecaCommand, mockOs, mockFs } = t.context
67
+ const { sandbox, mockLogger, mockTestcwd, mockConfig, mockBinPath, mockFs } = t.context
76
68
 
77
69
  const { default: InfoCommand } = await esmock('./info.js', {
78
70
  '#helpers/test-cwd.js': {
79
71
  default: mockTestcwd
80
72
  },
81
- 'execa': {
82
- execaCommand: mockExecaCommand
73
+ '#src/packageConfig.js': {
74
+ binPath: mockBinPath
83
75
  },
84
- 'node:os': mockOs,
85
76
  'node:fs': mockFs
86
77
  }, {
87
78
  '#lib/logger/index.js': {
@@ -108,16 +99,15 @@ test('info command should validate Quire project in preAction', async (t) => {
108
99
  })
109
100
 
110
101
  test('info command should display project version information', async (t) => {
111
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
102
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
112
103
 
113
104
  const { default: InfoCommand } = await esmock('./info.js', {
114
105
  '#helpers/test-cwd.js': {
115
106
  default: mockTestcwd
116
107
  },
117
- 'execa': {
118
- execaCommand: mockExecaCommand
108
+ '#src/packageConfig.js': {
109
+ binPath: mockBinPath
119
110
  },
120
- 'node:os': mockOs,
121
111
  'node:fs': mockFs
122
112
  }, {
123
113
  '#lib/logger/index.js': {
@@ -135,31 +125,42 @@ test('info command should display project version information', async (t) => {
135
125
  // Verify logger.info was called with project information
136
126
  t.true(mockLogger.info.called, 'logger.info should be called')
137
127
 
138
- // Check for project section header (uses current directory name)
128
+ // Check for project header
139
129
  const calls = mockLogger.info.getCalls()
140
130
  const hasProjectInfo = calls.some((call) =>
141
- call.args[0] && call.args[0].includes('[') && call.args[0].includes(']')
131
+ call.args[0] && call.args[0].includes('Project:')
142
132
  )
143
- t.true(hasProjectInfo, 'should display project section')
133
+ t.true(hasProjectInfo, 'should display project header')
144
134
 
145
135
  // Check for quire-cli version in output
146
136
  const hasCliVersion = calls.some((call) =>
147
137
  call.args[0] && call.args[0].includes('quire-cli') && call.args[0].includes('1.0.0-rc.33')
148
138
  )
149
139
  t.true(hasCliVersion, 'should display quire-cli version')
140
+
141
+ // Check for quire-11ty version in output
142
+ const has11tyVersion = calls.some((call) =>
143
+ call.args[0] && call.args[0].includes('quire-11ty')
144
+ )
145
+ t.true(has11tyVersion, 'should display quire-11ty version')
146
+
147
+ // Check for doctor tip
148
+ const hasDoctorTip = calls.some((call) =>
149
+ call.args[0] && call.args[0].includes('quire doctor')
150
+ )
151
+ t.true(hasDoctorTip, 'should display doctor tip')
150
152
  })
151
153
 
152
- test('info command should display system information with debug flag', async (t) => {
153
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
154
+ test('info command should display starter version when available', async (t) => {
155
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
154
156
 
155
157
  const { default: InfoCommand } = await esmock('./info.js', {
156
158
  '#helpers/test-cwd.js': {
157
159
  default: mockTestcwd
158
160
  },
159
- 'execa': {
160
- execaCommand: mockExecaCommand
161
+ '#src/packageConfig.js': {
162
+ binPath: mockBinPath
161
163
  },
162
- 'node:os': mockOs,
163
164
  'node:fs': mockFs
164
165
  }, {
165
166
  '#lib/logger/index.js': {
@@ -172,42 +173,63 @@ test('info command should display system information with debug flag', async (t)
172
173
  command.logger = mockLogger
173
174
  command.debug = sandbox.stub()
174
175
 
175
- await command.action({ debug: true }, {})
176
+ await command.action({}, {})
176
177
 
177
178
  // Verify logger.info was called
178
179
  t.true(mockLogger.info.called, 'logger.info should be called')
179
180
 
180
- // Check for system section header
181
+ // Check for starter version in output (from version file)
181
182
  const calls = mockLogger.info.getCalls()
182
- const hasSystemInfo = calls.some((call) =>
183
- call.args[0] && call.args[0].includes('[System]')
183
+ const hasStarterVersion = calls.some((call) =>
184
+ call.args[0] && call.args[0].includes('starter')
184
185
  )
185
- t.true(hasSystemInfo, 'should display system section with debug flag')
186
+ t.true(hasStarterVersion, 'should display starter version when available')
187
+ })
186
188
 
187
- // Check for node version in output (only shown with debug)
188
- const hasNodeVersion = calls.some((call) =>
189
- call.args[0] && call.args[0].includes('node')
190
- )
191
- t.true(hasNodeVersion, 'should display node version with debug flag')
189
+ test('info command --debug shows installation paths', async (t) => {
190
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
192
191
 
193
- // Check for npm version in output (only shown with debug)
194
- const hasNpmVersion = calls.some((call) =>
195
- call.args[0] && call.args[0].includes('npm')
196
- )
197
- t.true(hasNpmVersion, 'should display npm version with debug flag')
192
+ const { default: InfoCommand } = await esmock('./info.js', {
193
+ '#helpers/test-cwd.js': {
194
+ default: mockTestcwd
195
+ },
196
+ '#src/packageConfig.js': {
197
+ binPath: mockBinPath
198
+ },
199
+ 'node:fs': mockFs
200
+ }, {
201
+ '#lib/logger/index.js': {
202
+ default: () => mockLogger
203
+ }
204
+ })
205
+
206
+ const command = new InfoCommand()
207
+ command.config = mockConfig
208
+ command.logger = mockLogger
209
+ command.debug = sandbox.stub()
210
+
211
+ await command.action({ debug: true }, {})
212
+
213
+ const calls = mockLogger.info.getCalls()
214
+ const output = calls.map((call) => call.args[0]).join('\n')
215
+
216
+ // Check for quire-cli path
217
+ t.true(output.includes('/usr/local/bin/quire'), 'should display quire-cli path')
198
218
  })
199
219
 
200
- test('info command should hide system details without debug flag', async (t) => {
201
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
220
+ test('info command --debug handles missing quire-cli in PATH', async (t) => {
221
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockFs } = t.context
222
+
223
+ // binPath returns null when executable is not found
224
+ const mockBinPathNotFound = sandbox.stub().returns(null)
202
225
 
203
226
  const { default: InfoCommand } = await esmock('./info.js', {
204
227
  '#helpers/test-cwd.js': {
205
228
  default: mockTestcwd
206
229
  },
207
- 'execa': {
208
- execaCommand: mockExecaCommand
230
+ '#src/packageConfig.js': {
231
+ binPath: mockBinPathNotFound
209
232
  },
210
- 'node:os': mockOs,
211
233
  'node:fs': mockFs
212
234
  }, {
213
235
  '#lib/logger/index.js': {
@@ -220,23 +242,16 @@ test('info command should hide system details without debug flag', async (t) =>
220
242
  command.logger = mockLogger
221
243
  command.debug = sandbox.stub()
222
244
 
223
- await command.action({}, {})
224
-
225
- // Verify logger.info was called
226
- t.true(mockLogger.info.called, 'logger.info should be called')
245
+ await command.action({ debug: true }, {})
227
246
 
228
- // Check that system details are NOT shown without debug flag
229
247
  const calls = mockLogger.info.getCalls()
248
+ const output = calls.map((call) => call.args[0]).join('\n')
230
249
 
231
- // Node version should not appear in non-debug output
232
- const hasNodeVersion = calls.some((call) =>
233
- call.args[0] && call.args[0].toLowerCase().includes('node') && !call.args[0].includes('quire')
234
- )
235
- t.false(hasNodeVersion, 'should not display node version without debug flag')
250
+ t.true(output.includes('not found in PATH'), 'should show not found message for quire-cli')
236
251
  })
237
252
 
238
253
  test('info command should handle missing version file', async (t) => {
239
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
254
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
240
255
 
241
256
  // Setup mockFs to throw error for missing version file (simulating file not found)
242
257
  mockFs.readFileSync.callsFake((filePath, options) => {
@@ -253,10 +268,9 @@ test('info command should handle missing version file', async (t) => {
253
268
  '#helpers/test-cwd.js': {
254
269
  default: mockTestcwd
255
270
  },
256
- 'execa': {
257
- execaCommand: mockExecaCommand
271
+ '#src/packageConfig.js': {
272
+ binPath: mockBinPath
258
273
  },
259
- 'node:os': mockOs,
260
274
  'node:fs': mockFs
261
275
  }, {
262
276
  '#lib/logger/index.js': {
@@ -283,7 +297,7 @@ test('info command should handle missing version file', async (t) => {
283
297
  })
284
298
 
285
299
  test('info command should handle malformed version file', async (t) => {
286
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
300
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
287
301
 
288
302
  // Setup mockFs to return malformed JSON (simulating corrupted file)
289
303
  mockFs.readFileSync.callsFake((filePath, options) => {
@@ -300,10 +314,9 @@ test('info command should handle malformed version file', async (t) => {
300
314
  '#helpers/test-cwd.js': {
301
315
  default: mockTestcwd
302
316
  },
303
- 'execa': {
304
- execaCommand: mockExecaCommand
317
+ '#src/packageConfig.js': {
318
+ binPath: mockBinPath
305
319
  },
306
- 'node:os': mockOs,
307
320
  'node:fs': mockFs
308
321
  }, {
309
322
  '#lib/logger/index.js': {
@@ -333,7 +346,7 @@ test('info command should handle malformed version file', async (t) => {
333
346
  })
334
347
 
335
348
  test('info command should read quire-11ty version from package.json', async (t) => {
336
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
349
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
337
350
 
338
351
  // Override package.json to have specific version
339
352
  mockFs.readFileSync.callsFake((filePath, options) => {
@@ -353,10 +366,9 @@ test('info command should read quire-11ty version from package.json', async (t)
353
366
  '#helpers/test-cwd.js': {
354
367
  default: mockTestcwd
355
368
  },
356
- 'execa': {
357
- execaCommand: mockExecaCommand
369
+ '#src/packageConfig.js': {
370
+ binPath: mockBinPath
358
371
  },
359
- 'node:os': mockOs,
360
372
  'node:fs': mockFs
361
373
  }, {
362
374
  '#lib/logger/index.js': {
@@ -382,8 +394,8 @@ test('info command should read quire-11ty version from package.json', async (t)
382
394
  t.true(hasEleventyVersion, 'should display quire-11ty version from package.json')
383
395
  })
384
396
 
385
- test('info command should log debug information when debug flag is set', async (t) => {
386
- const { sandbox, mockLogger, mockConfig, mockTestcwd, mockExecaCommand, mockOs, mockFs } = t.context
397
+ test('info command should log debug information', async (t) => {
398
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
387
399
 
388
400
  const mockDebug = sandbox.stub()
389
401
 
@@ -391,10 +403,9 @@ test('info command should log debug information when debug flag is set', async (
391
403
  '#helpers/test-cwd.js': {
392
404
  default: mockTestcwd
393
405
  },
394
- 'execa': {
395
- execaCommand: mockExecaCommand
406
+ '#src/packageConfig.js': {
407
+ binPath: mockBinPath
396
408
  },
397
- 'node:os': mockOs,
398
409
  'node:fs': mockFs
399
410
  }, {
400
411
  '#lib/logger/index.js': {
@@ -407,9 +418,95 @@ test('info command should log debug information when debug flag is set', async (
407
418
  command.logger = mockLogger
408
419
  command.debug = mockDebug
409
420
 
410
- await command.action({ debug: true }, {})
421
+ await command.action({}, {})
411
422
 
412
423
  // Verify debug was called
413
- t.true(mockDebug.called, 'debug should be called with debug flag')
414
- t.true(mockDebug.calledWith('called with options %O', { debug: true }))
424
+ t.true(mockDebug.called, 'debug should be called')
425
+ t.true(mockDebug.calledWith('called with options %O', {}))
426
+ })
427
+
428
+ // ─────────────────────────────────────────────────────────────────────────────
429
+ // JSON output tests
430
+ // ─────────────────────────────────────────────────────────────────────────────
431
+
432
+ test.serial('info --json should output valid JSON with project versions', async (t) => {
433
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
434
+ const consoleLogStub = sandbox.stub(console, 'log')
435
+
436
+ const { default: InfoCommand } = await esmock('./info.js', {
437
+ '#helpers/test-cwd.js': {
438
+ default: mockTestcwd
439
+ },
440
+ '#src/packageConfig.js': {
441
+ binPath: mockBinPath
442
+ },
443
+ 'node:fs': mockFs
444
+ }, {
445
+ '#lib/logger/index.js': {
446
+ default: () => mockLogger
447
+ }
448
+ })
449
+
450
+ const command = new InfoCommand()
451
+ command.config = mockConfig
452
+ command.logger = mockLogger
453
+ command.debug = sandbox.stub()
454
+
455
+ await command.action({ json: true }, {})
456
+
457
+ // Should output via console.log, not logger.info
458
+ t.true(consoleLogStub.calledOnce, 'console.log should be called once')
459
+ t.false(mockLogger.info.called, 'logger.info should not be called in JSON mode')
460
+
461
+ // Parse and validate the JSON structure
462
+ const output = JSON.parse(consoleLogStub.firstCall.args[0])
463
+
464
+ t.truthy(output.project, 'JSON should have project section')
465
+
466
+ // Project section
467
+ t.is(output.project.cli, '1.0.0-rc.33')
468
+ t.is(output.project.quire11ty, '1.0.0')
469
+ t.is(output.project.starter, 'https://github.com/thegetty/quire-starter-default')
470
+ t.is(typeof output.project.directory, 'string')
471
+ })
472
+
473
+ test.serial('info --json should set starter to null when not available', async (t) => {
474
+ const { sandbox, mockLogger, mockConfig, mockTestcwd, mockBinPath, mockFs } = t.context
475
+ const consoleLogStub = sandbox.stub(console, 'log')
476
+
477
+ // Version file without starter
478
+ t.context.mockFs.readFileSync.callsFake((filePath, options) => {
479
+ if (filePath === './.quire-version') {
480
+ return JSON.stringify({ cli: '1.0.0-rc.33' })
481
+ }
482
+ if (filePath === './package.json') {
483
+ return t.context.packageJsonContent
484
+ }
485
+ return ''
486
+ })
487
+
488
+ const { default: InfoCommand } = await esmock('./info.js', {
489
+ '#helpers/test-cwd.js': {
490
+ default: mockTestcwd
491
+ },
492
+ '#src/packageConfig.js': {
493
+ binPath: mockBinPath
494
+ },
495
+ 'node:fs': mockFs
496
+ }, {
497
+ '#lib/logger/index.js': {
498
+ default: () => mockLogger
499
+ }
500
+ })
501
+
502
+ const command = new InfoCommand()
503
+ command.config = mockConfig
504
+ command.logger = mockLogger
505
+ command.debug = sandbox.stub()
506
+
507
+ await command.action({ json: true }, {})
508
+
509
+ const output = JSON.parse(consoleLogStub.firstCall.args[0])
510
+
511
+ t.is(output.project.starter, null, 'starter should be null when not in version file')
415
512
  })
@@ -5,6 +5,7 @@ import eleventy from '#lib/11ty/index.js'
5
5
  import generatePdf, { ENGINES } from '#lib/pdf/index.js'
6
6
  import open from 'open'
7
7
  import path from 'node:path'
8
+ import { recordStatus } from '#lib/conf/build-status.js'
8
9
  import reporter from '#lib/reporter/index.js'
9
10
  import testcwd from '#helpers/test-cwd.js'
10
11
  import { MissingBuildOutputError } from '#src/errors/index.js'
@@ -81,10 +82,16 @@ Examples:
81
82
 
82
83
  // Generate PDF - façade handles validation, progress, and errors
83
84
  const pdfOptions = { ...options, lib: options.engine }
84
- const output = await generatePdf(pdfOptions)
85
85
 
86
- if (options.open) {
87
- open(output)
86
+ try {
87
+ const output = await generatePdf(pdfOptions)
88
+ recordStatus(paths.getProjectRoot(), 'pdf', 'ok')
89
+ if (options.open) {
90
+ open(output)
91
+ }
92
+ } catch (error) {
93
+ recordStatus(paths.getProjectRoot(), 'pdf', 'failed')
94
+ throw error
88
95
  }
89
96
  }
90
97