@thegetty/quire-cli 1.0.0-rc.35 → 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 (158) 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 +46 -37
  14. package/src/commands/build.spec.js +113 -0
  15. package/src/commands/build.test.js +402 -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 +26 -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 +176 -93
  75. package/src/lib/11ty/cli.js +77 -35
  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 +90 -0
  112. package/src/lib/logger/debug.spec.js +130 -0
  113. package/src/lib/logger/index.js +228 -0
  114. package/src/lib/logger/index.spec.js +131 -0
  115. package/src/lib/logger/index.test.js +477 -0
  116. package/src/lib/npm/README.md +127 -0
  117. package/src/lib/npm/index.js +198 -0
  118. package/src/lib/npm/index.spec.js +60 -0
  119. package/src/lib/npm/index.test.js +355 -0
  120. package/src/lib/pdf/README.md +131 -0
  121. package/src/lib/pdf/engines.js +46 -0
  122. package/src/lib/pdf/index.js +124 -21
  123. package/src/lib/pdf/index.test.js +708 -0
  124. package/src/lib/pdf/paged.js +100 -52
  125. package/src/lib/pdf/paged.test.js +366 -0
  126. package/src/lib/pdf/prince.js +115 -37
  127. package/src/lib/pdf/prince.test.js +202 -0
  128. package/src/lib/pdf/schema.js +21 -0
  129. package/src/lib/pdf/split.js +61 -33
  130. package/src/lib/pdf/split.test.js +445 -0
  131. package/src/lib/process/manager.js +110 -0
  132. package/src/lib/process/manager.test.js +55 -0
  133. package/src/lib/project/build.js +143 -0
  134. package/src/lib/project/build.test.js +253 -0
  135. package/src/lib/project/config.js +48 -0
  136. package/src/lib/project/config.test.js +134 -0
  137. package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
  138. package/src/lib/project/detect.test.js +157 -0
  139. package/src/lib/project/index.js +40 -0
  140. package/src/lib/project/paths.js +224 -0
  141. package/src/lib/project/version.js +110 -0
  142. package/src/lib/project/version.test.js +350 -0
  143. package/src/lib/reporter/README.md +211 -2
  144. package/src/lib/reporter/index.js +512 -0
  145. package/src/lib/reporter/index.test.js +593 -0
  146. package/src/main.js +134 -47
  147. package/src/main.spec.js +61 -0
  148. package/src/main.test.js +347 -0
  149. package/src/validators/utils.js +2 -1
  150. package/src/commands/conf.js +0 -43
  151. package/src/commands/version.js +0 -43
  152. package/src/lib/11ty/paths.js +0 -103
  153. package/src/lib/i18n/README.md +0 -3
  154. package/src/lib/i18n/config.js +0 -53
  155. package/src/lib/i18n/index.js +0 -43
  156. package/src/lib/i18n/localeService.js +0 -58
  157. package/src/lib/quire/README.md +0 -19
  158. package/src/lib/quire/index.js +0 -350
@@ -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
+ }
@@ -0,0 +1,107 @@
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
+ // Command is named 'settings' with 'conf' as an alias
15
+ t.context.command = program.commands.find((cmd) => cmd.name() === 'settings')
16
+ })
17
+
18
+ test('command is registered in CLI program', (t) => {
19
+ const { command } = t.context
20
+
21
+ t.truthy(command, 'command "settings" should be registered in program')
22
+ t.true(command instanceof Command, 'registered command should be Commander.js Command instance')
23
+ })
24
+
25
+ test('registered command has correct metadata', (t) => {
26
+ const { command } = t.context
27
+
28
+ t.is(command.name(), 'settings')
29
+ t.truthy(command.description())
30
+ t.is(typeof command._actionHandler, 'function', 'command should have action handler')
31
+ })
32
+
33
+ test('registered command has correct aliases', (t) => {
34
+ const { command } = t.context
35
+
36
+ t.truthy(command.aliases)
37
+ t.true(Array.isArray(command.aliases()))
38
+ t.true(command.aliases().includes('prefs'))
39
+ t.true(command.aliases().includes('preferences'))
40
+ t.true(command.aliases().includes('conf'))
41
+ t.true(command.aliases().includes('config'))
42
+ t.true(command.aliases().includes('configure'))
43
+ })
44
+
45
+ test('registered command has correct arguments', (t) => {
46
+ const { command } = t.context
47
+ const registeredArguments = command.registeredArguments
48
+
49
+ t.is(registeredArguments.length, 3, 'conf command should have 3 arguments')
50
+
51
+ // Verify operation argument
52
+ const operationArg = registeredArguments[0]
53
+ t.is(operationArg.name(), 'operation')
54
+ t.false(operationArg.required, 'operation argument should be optional')
55
+
56
+ // Verify key argument
57
+ const keyArg = registeredArguments[1]
58
+ t.is(keyArg.name(), 'key')
59
+ t.false(keyArg.required, 'key argument should be optional')
60
+
61
+ // Verify value argument
62
+ const valueArg = registeredArguments[2]
63
+ t.is(valueArg.name(), 'value')
64
+ t.false(valueArg.required, 'value argument should be optional')
65
+ })
66
+
67
+ test('registered command has correct options', (t) => {
68
+ const { command } = t.context
69
+
70
+ // Get all options
71
+ const jsonOption = command.options.find((opt) => opt.long === '--json')
72
+ const debugOption = command.options.find((opt) => opt.long === '--debug')
73
+
74
+ // Verify --json option exists
75
+ t.truthy(jsonOption, '--json option should exist')
76
+ t.true(jsonOption instanceof Option, '--json should be Option instance')
77
+ t.is(jsonOption.long, '--json')
78
+ t.truthy(jsonOption.description)
79
+ t.false(jsonOption.required, '--json should not require a value')
80
+
81
+ // Verify --debug option exists
82
+ t.truthy(debugOption, '--debug option should exist')
83
+ t.true(debugOption instanceof Option, '--debug should be Option instance')
84
+ t.is(debugOption.long, '--debug')
85
+ t.truthy(debugOption.description)
86
+ t.false(debugOption.required, '--debug should not require a value')
87
+
88
+ // Verify no operation flags exist (operations are via positional argument)
89
+ const deleteOption = command.options.find((opt) => opt.long === '--delete')
90
+ const resetOption = command.options.find((opt) => opt.long === '--reset')
91
+ const pathOption = command.options.find((opt) => opt.long === '--path')
92
+
93
+ t.falsy(deleteOption, '--delete option should not exist (use delete operation)')
94
+ t.falsy(resetOption, '--reset option should not exist (use reset operation)')
95
+ t.falsy(pathOption, '--path option should not exist (use path operation)')
96
+ })
97
+
98
+ test('command options are accessible via public API', (t) => {
99
+ const { command } = t.context
100
+
101
+ // Test that options can be accessed the way Commander.js does
102
+ const optionNames = command.options.map((opt) => opt.long)
103
+
104
+ t.true(optionNames.includes('--json'))
105
+ t.true(optionNames.includes('--debug'))
106
+ t.is(optionNames.length, 2, 'should have --json and --debug options')
107
+ })