@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,108 @@
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() === 'clean')
15
+ })
16
+
17
+ test('command is registered in CLI program', (t) => {
18
+ const { command } = t.context
19
+
20
+ t.truthy(command, 'command "clean" 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(), 'clean')
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, 'clean 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 dryRunOption = command.options.find((opt) => opt.long === '--dry-run')
44
+ const dryrunAlias = command.options.find((opt) => opt.long === '--dryrun')
45
+ const progressOption = command.options.find((opt) => opt.long === '--progress')
46
+ const quietOption = command.options.find((opt) => opt.long === '--quiet')
47
+ const verboseOption = command.options.find((opt) => opt.long === '--verbose')
48
+ const debugOption = command.options.find((opt) => opt.long === '--debug')
49
+
50
+ // Verify all options exist
51
+ t.truthy(dryRunOption, '--dry-run option should exist')
52
+ t.truthy(dryrunAlias, '--dryrun alias should exist')
53
+ t.truthy(progressOption, '--progress option should exist')
54
+ t.truthy(quietOption, '--quiet option should exist')
55
+ t.truthy(verboseOption, '--verbose option should exist')
56
+ t.truthy(debugOption, '--debug option should exist')
57
+
58
+ // Verify they are Option instances
59
+ t.true(dryRunOption instanceof Option, '--dry-run should be Option instance')
60
+ t.true(dryrunAlias instanceof Option, '--dryrun should be Option instance')
61
+ t.true(progressOption instanceof Option, '--progress should be Option instance')
62
+ t.true(quietOption instanceof Option, '--quiet should be Option instance')
63
+ t.true(verboseOption instanceof Option, '--verbose should be Option instance')
64
+ t.true(debugOption instanceof Option, '--debug should be Option instance')
65
+
66
+ // Verify option properties
67
+ t.is(dryRunOption.long, '--dry-run')
68
+ t.is(dryRunOption.short, '-d')
69
+ t.truthy(dryRunOption.description)
70
+ t.false(dryRunOption.required, '--dry-run should not require a value')
71
+
72
+ // --dryrun is a hidden alias for --dry-run
73
+ t.is(dryrunAlias.long, '--dryrun')
74
+ t.true(dryrunAlias.hidden, '--dryrun should be hidden from help')
75
+
76
+ // --progress is a hidden alias for --verbose
77
+ t.is(progressOption.long, '--progress')
78
+ t.truthy(progressOption.description)
79
+ t.true(progressOption.hidden, '--progress should be hidden from help')
80
+
81
+ t.is(quietOption.long, '--quiet')
82
+ t.is(quietOption.short, '-q')
83
+ t.truthy(quietOption.description)
84
+ t.false(quietOption.required, '--quiet should not require a value')
85
+
86
+ t.is(verboseOption.long, '--verbose')
87
+ t.is(verboseOption.short, '-v')
88
+ t.truthy(verboseOption.description)
89
+ t.false(verboseOption.required, '--verbose should not require a value')
90
+
91
+ t.is(debugOption.long, '--debug')
92
+ t.truthy(debugOption.description)
93
+ t.false(debugOption.required, '--debug should not require a value')
94
+ })
95
+
96
+ test('command options are accessible via public API', (t) => {
97
+ const { command } = t.context
98
+
99
+ // Test that options can be accessed the way Commander.js does
100
+ const optionNames = command.options.map((opt) => opt.long)
101
+
102
+ t.true(optionNames.includes('--dry-run'))
103
+ t.true(optionNames.includes('--dryrun'))
104
+ t.true(optionNames.includes('--progress'))
105
+ t.true(optionNames.includes('--quiet'))
106
+ t.true(optionNames.includes('--verbose'))
107
+ t.true(optionNames.includes('--debug'))
108
+ })
@@ -0,0 +1,260 @@
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 directory structure with build artifacts
15
+ t.context.vol.fromJSON({
16
+ '/project/_site/index.html': '<html>Quire Test </html>',
17
+ '/project/_site/styles.css': 'body { }',
18
+ '/project/content/index.md': '# Content',
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
+ })
34
+
35
+ test.afterEach.always((t) => {
36
+ // Restore all mocks
37
+ t.context.sandbox.restore()
38
+
39
+ // Clear in-memory file system
40
+ t.context.vol.reset()
41
+ })
42
+
43
+ test('clean command should call clean helper with correct parameters', async (t) => {
44
+ const { sandbox, fs, mockLogger } = t.context
45
+
46
+ // Mock clean helper
47
+ const mockClean = sandbox.stub().resolves(['/project/_site'])
48
+
49
+ // Mock testcwd helper
50
+ const mockTestcwd = sandbox.stub()
51
+
52
+ // Use esmock to replace imports
53
+ const CleanCommand = await esmock('./clean.js', {
54
+ '#helpers/clean.js': {
55
+ clean: mockClean
56
+ },
57
+ '#lib/project/index.js': {
58
+ default: {
59
+ getProjectRoot: () => '/project',
60
+ toObject: () => ({ output: '_site' })
61
+ }
62
+ },
63
+ '#helpers/test-cwd.js': {
64
+ default: mockTestcwd
65
+ },
66
+ '#lib/logger/index.js': {
67
+ logger: mockLogger
68
+ },
69
+ 'fs-extra': fs
70
+ })
71
+
72
+ const command = new CleanCommand()
73
+ command.name = sandbox.stub().returns('clean')
74
+ command.logger = mockLogger
75
+ command.debug = sandbox.stub()
76
+
77
+ // Run action
78
+ const options = {}
79
+ await command.action(options, command)
80
+
81
+ t.true(mockClean.called, 'clean should be called')
82
+ t.true(mockClean.calledWith('/project', { output: '_site' }, options), 'clean should be called with correct parameters')
83
+
84
+ // Verify user-visible output
85
+ t.true(mockLogger.info.called, 'logger.info should be called with result')
86
+ const output = mockLogger.info.firstCall.args[0]
87
+ t.true(output.includes('Deleted'), 'should report deleted files')
88
+ t.true(output.includes('/project/_site'), 'should list deleted paths')
89
+ })
90
+
91
+ test('clean command should handle dry-run option', async (t) => {
92
+ const { sandbox, fs, mockLogger } = t.context
93
+
94
+ // Mock clean helper to return paths that would be deleted
95
+ const mockClean = sandbox.stub().resolves(['/project/_site/index.html', '/project/_site/styles.css'])
96
+
97
+ // Mock testcwd helper
98
+ const mockTestcwd = sandbox.stub()
99
+
100
+ // Use esmock to replace imports
101
+ const CleanCommand = await esmock('./clean.js', {
102
+ '#helpers/clean.js': {
103
+ clean: mockClean
104
+ },
105
+ '#lib/project/index.js': {
106
+ default: {
107
+ getProjectRoot: () => '/project',
108
+ toObject: () => ({ output: '_site' })
109
+ }
110
+ },
111
+ '#helpers/test-cwd.js': {
112
+ default: mockTestcwd
113
+ },
114
+ '#lib/logger/index.js': {
115
+ logger: mockLogger
116
+ },
117
+ 'fs-extra': fs
118
+ })
119
+
120
+ const command = new CleanCommand()
121
+ command.name = sandbox.stub().returns('clean')
122
+ command.logger = mockLogger
123
+ command.debug = sandbox.stub()
124
+
125
+ // Run action with dry-run
126
+ const options = { dryRun: true }
127
+ await command.action(options, command)
128
+
129
+ t.true(mockClean.called, 'clean should be called')
130
+ t.true(mockClean.calledWith('/project', { output: '_site' }, options), 'clean should receive dry-run option')
131
+
132
+ // Verify dry-run output uses "Will delete" instead of "Deleted"
133
+ const output = mockLogger.info.firstCall.args[0]
134
+ t.true(output.includes('Will delete'), 'should use future tense for dry-run')
135
+ })
136
+
137
+ test('clean command should call testcwd in preAction', async (t) => {
138
+ const { sandbox, fs, mockLogger } = t.context
139
+
140
+ // Mock clean helper
141
+ const mockClean = sandbox.stub().resolves([])
142
+
143
+ // Mock testcwd helper
144
+ const mockTestcwd = sandbox.stub()
145
+
146
+ // Use esmock to replace imports
147
+ const CleanCommand = await esmock('./clean.js', {
148
+ '#helpers/clean.js': {
149
+ clean: mockClean
150
+ },
151
+ '#lib/project/index.js': {
152
+ default: {
153
+ getProjectRoot: () => '/project',
154
+ toObject: () => ({ output: '_site' })
155
+ }
156
+ },
157
+ '#helpers/test-cwd.js': {
158
+ default: mockTestcwd
159
+ },
160
+ '#lib/logger/index.js': {
161
+ logger: mockLogger
162
+ },
163
+ 'fs-extra': fs
164
+ })
165
+
166
+ const command = new CleanCommand()
167
+ command.name = sandbox.stub().returns('clean')
168
+
169
+ // Run preAction
170
+ command.preAction(command)
171
+
172
+ t.true(mockTestcwd.called, 'testcwd should be called in preAction')
173
+ t.true(mockTestcwd.calledWith(command), 'testcwd should be called with command object')
174
+ })
175
+
176
+ test('clean command should handle empty deletedPaths', async (t) => {
177
+ const { sandbox, fs, mockLogger } = t.context
178
+
179
+ // Mock clean helper to return empty array (no files to delete)
180
+ const mockClean = sandbox.stub().resolves([])
181
+
182
+ // Mock testcwd helper
183
+ const mockTestcwd = sandbox.stub()
184
+
185
+ // Use esmock to replace imports
186
+ const CleanCommand = await esmock('./clean.js', {
187
+ '#helpers/clean.js': {
188
+ clean: mockClean
189
+ },
190
+ '#lib/project/index.js': {
191
+ default: {
192
+ getProjectRoot: () => '/project',
193
+ toObject: () => ({ output: '_site' })
194
+ }
195
+ },
196
+ '#helpers/test-cwd.js': {
197
+ default: mockTestcwd
198
+ },
199
+ '#lib/logger/index.js': {
200
+ logger: mockLogger
201
+ },
202
+ 'fs-extra': fs
203
+ })
204
+
205
+ const command = new CleanCommand()
206
+ command.name = sandbox.stub().returns('clean')
207
+ command.logger = mockLogger
208
+ command.debug = sandbox.stub()
209
+
210
+ // Run action
211
+ const options = {}
212
+ await command.action(options, command)
213
+
214
+ t.true(mockClean.called, 'clean should be called')
215
+
216
+ // Verify "no files" message
217
+ const output = mockLogger.info.firstCall.args[0]
218
+ t.true(output.includes('No files to delete'), 'should report no files to delete')
219
+ })
220
+
221
+ test('clean command should pass all options to clean helper', async (t) => {
222
+ const { sandbox, fs, mockLogger } = t.context
223
+
224
+ // Mock clean helper
225
+ const mockClean = sandbox.stub().resolves(['/project/_site'])
226
+
227
+ // Mock testcwd helper
228
+ const mockTestcwd = sandbox.stub()
229
+
230
+ // Use esmock to replace imports
231
+ const CleanCommand = await esmock('./clean.js', {
232
+ '#helpers/clean.js': {
233
+ clean: mockClean
234
+ },
235
+ '#lib/project/index.js': {
236
+ default: {
237
+ getProjectRoot: () => '/project',
238
+ toObject: () => ({ output: '_site' })
239
+ }
240
+ },
241
+ '#helpers/test-cwd.js': {
242
+ default: mockTestcwd
243
+ },
244
+ '#lib/logger/index.js': {
245
+ logger: mockLogger
246
+ },
247
+ 'fs-extra': fs
248
+ })
249
+
250
+ const command = new CleanCommand()
251
+ command.name = sandbox.stub().returns('clean')
252
+ command.logger = mockLogger
253
+ command.debug = sandbox.stub()
254
+
255
+ // Run action with multiple options
256
+ const options = { verbose: true, dryRun: false }
257
+ await command.action(options, command)
258
+
259
+ t.true(mockClean.calledWith('/project', { output: '_site' }, options), 'clean should receive all options')
260
+ })
@@ -0,0 +1,251 @@
1
+ import Command from '#src/Command.js'
2
+ import {
3
+ isValidKey,
4
+ getValidKeys,
5
+ coerceValue,
6
+ formatValidationError,
7
+ getDefault,
8
+ formatSettings,
9
+ } from '#lib/conf/index.js'
10
+
11
+ /**
12
+ * Valid operations for the settings command
13
+ */
14
+ const OPERATIONS = Object.freeze(['get', 'set', 'delete', 'reset', 'path'])
15
+
16
+ /**
17
+ * Quire CLI `settings` Command
18
+ *
19
+ * Manages quire-cli configuration with explicit subcommand operations.
20
+ *
21
+ * @example
22
+ * // Show all settings
23
+ * quire settings
24
+ *
25
+ * // Get a single value
26
+ * quire settings get logLevel
27
+ *
28
+ * // Set a value
29
+ * quire settings set logLevel debug
30
+ *
31
+ * // Delete a key (reset to default)
32
+ * quire settings delete logLevel
33
+ *
34
+ * // Reset all settings to defaults
35
+ * quire settings reset
36
+ *
37
+ * // Reset a single key to default
38
+ * quire settings reset logLevel
39
+ *
40
+ * // Show settings file path
41
+ * quire settings path
42
+ *
43
+ * @class SettingsCommand
44
+ * @extends {Command}
45
+ */
46
+ export default class SettingsCommand extends Command {
47
+ static definition = {
48
+ name: 'settings',
49
+ aliases: ['prefs', 'preferences', 'conf', 'config', 'configure'],
50
+ description: 'Manage Quire CLI settings.',
51
+ summary: 'view and modify CLI settings',
52
+ docsLink: 'quire-commands/',
53
+ helpText: `
54
+ Examples:
55
+ quire settings Show all settings
56
+ quire settings get <key> Get a single value
57
+ quire settings set <key> <value> Set a value
58
+ quire settings delete <key> Delete (reset to default)
59
+ quire settings reset [key] Reset all or single key
60
+ quire settings path Show settings file path
61
+ quire settings --json Output raw config JSON
62
+ quire settings get <key> --json Output single value as JSON
63
+ `,
64
+ version: '2.0.0',
65
+ args: [
66
+ ['[operation]', `operation to perform (${OPERATIONS.join(', ')})`],
67
+ ['[key]', 'configuration key'],
68
+ ['[value]', 'value to set (for set operation)'],
69
+ ],
70
+ options: [
71
+ ['--json', 'output raw JSON'],
72
+ ['--debug', 'enable debug output for troubleshooting'],
73
+ ],
74
+ }
75
+
76
+ constructor() {
77
+ super(SettingsCommand.definition)
78
+ }
79
+
80
+ /**
81
+ * Log an unknown key error with valid keys hint
82
+ *
83
+ * @param {string} key - The unknown configuration key
84
+ */
85
+ #logUnknownKey(key) {
86
+ this.logger.error(`Unknown configuration key: ${key}`)
87
+ this.logger.info(`Valid keys: ${getValidKeys().join(', ')}`)
88
+ }
89
+
90
+ /**
91
+ * Get a single configuration value
92
+ *
93
+ * @param {string} key - Configuration key
94
+ * @param {Object} options - Command options
95
+ */
96
+ #getValue(key, options) {
97
+ if (!key) {
98
+ this.logger.error('Usage: quire conf get <key>')
99
+ return
100
+ }
101
+
102
+ if (!isValidKey(key) && !key.startsWith('__internal__')) {
103
+ this.#logUnknownKey(key)
104
+ return
105
+ }
106
+
107
+ const value = this.config.get(key)
108
+
109
+ if (options.json) {
110
+ console.log(JSON.stringify(value))
111
+ return
112
+ }
113
+
114
+ if (value === undefined) {
115
+ this.logger.info(`${key}: (not set)`)
116
+ } else {
117
+ this.logger.info(`${key}: ${JSON.stringify(value)}`)
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Set a configuration value
123
+ *
124
+ * @param {string} key - Configuration key
125
+ * @param {string} value - Value to set
126
+ */
127
+ #setValue(key, value) {
128
+ if (!key || value === undefined) {
129
+ this.logger.error('Usage: quire conf set <key> <value>')
130
+ return
131
+ }
132
+
133
+ if (!isValidKey(key)) {
134
+ this.#logUnknownKey(key)
135
+ return
136
+ }
137
+
138
+ const coercedValue = coerceValue(key, value)
139
+
140
+ try {
141
+ this.config.set(key, coercedValue)
142
+ this.logger.info(`${key}: ${JSON.stringify(coercedValue)}`)
143
+ } catch {
144
+ this.logger.error(formatValidationError(key, coercedValue))
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Delete a configuration key (resets to default)
150
+ *
151
+ * @param {string} key - Configuration key
152
+ */
153
+ #deleteKey(key) {
154
+ if (!key) {
155
+ this.logger.error('Usage: quire conf delete <key>')
156
+ return
157
+ }
158
+
159
+ if (!isValidKey(key)) {
160
+ this.#logUnknownKey(key)
161
+ return
162
+ }
163
+
164
+ this.config.delete(key)
165
+ const defaultValue = getDefault(key)
166
+ this.logger.info(`${key}: reset to ${JSON.stringify(defaultValue)}`)
167
+ }
168
+
169
+ /**
170
+ * Reset configuration to defaults
171
+ *
172
+ * @param {string} [key] - Optional key to reset (resets all if omitted)
173
+ */
174
+ #reset(key) {
175
+ if (key) {
176
+ if (!isValidKey(key)) {
177
+ this.#logUnknownKey(key)
178
+ return
179
+ }
180
+ this.config.reset(key)
181
+ const defaultValue = getDefault(key)
182
+ this.logger.info(`${key}: reset to ${JSON.stringify(defaultValue)}`)
183
+ } else {
184
+ this.config.clear()
185
+ this.logger.info('Configuration reset to defaults')
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Show all configuration values with descriptions
191
+ */
192
+ #showAll(options) {
193
+ if (options.json) {
194
+ const store = options.debug
195
+ ? this.config.store
196
+ : Object.fromEntries(
197
+ Object.entries(this.config.store).filter(([k]) => !k.startsWith('__internal__'))
198
+ )
199
+ console.log(JSON.stringify(store, null, 2))
200
+ return
201
+ }
202
+
203
+ const output = formatSettings(this.config.store, {
204
+ showInternal: options.debug,
205
+ configPath: this.config.path,
206
+ useColor: this.config.get('logUseColor'),
207
+ })
208
+ this.logger.info(output)
209
+ }
210
+
211
+ /**
212
+ * Show config file path
213
+ */
214
+ #showPath() {
215
+ this.logger.info(this.config.path)
216
+ }
217
+
218
+ /**
219
+ * @param {string} [operation] - Operation to perform
220
+ * @param {string} [key] - Configuration key
221
+ * @param {string} [value] - Value to set
222
+ * @param {Object} options - Command options
223
+ * @return {Promise}
224
+ */
225
+ async action(operation, key, value, options = {}) {
226
+ this.debug('called with operation=%s key=%s value=%s options=%O', operation, key, value, options)
227
+
228
+ // Validate operation if provided
229
+ if (operation && !OPERATIONS.includes(operation)) {
230
+ this.logger.error(`Unknown operation: ${operation}`)
231
+ this.logger.info(`Valid operations: ${OPERATIONS.join(', ')}`)
232
+ return
233
+ }
234
+
235
+ // Dispatch to operation handler
236
+ switch (operation) {
237
+ case 'get':
238
+ return this.#getValue(key, options)
239
+ case 'set':
240
+ return this.#setValue(key, value)
241
+ case 'delete':
242
+ return this.#deleteKey(key)
243
+ case 'reset':
244
+ return this.#reset(key)
245
+ case 'path':
246
+ return this.#showPath()
247
+ default:
248
+ return this.#showAll(options)
249
+ }
250
+ }
251
+ }