@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.
- package/CHANGELOG.md +36 -0
- package/README.md +9 -0
- package/bin/cli.js +19 -1
- package/package.json +21 -8
- package/patches/README.md +19 -0
- package/patches/install-npm-version+1.0.9.patch +12119 -0
- package/schemas/objects.schema.json +12 -38
- package/schemas/publication.schema.json +0 -22
- package/schemas/references.schema.json +0 -1
- package/src/Command.js +26 -6
- package/src/Command.spec.js +99 -0
- package/src/commands/README.md +213 -122
- package/src/commands/build.js +46 -37
- package/src/commands/build.spec.js +113 -0
- package/src/commands/build.test.js +402 -0
- package/src/commands/clean.js +25 -19
- package/src/commands/clean.spec.js +108 -0
- package/src/commands/clean.test.js +260 -0
- package/src/commands/config.js +251 -0
- package/src/commands/config.spec.js +107 -0
- package/src/commands/config.test.js +715 -0
- package/src/commands/create.js +42 -14
- package/src/commands/create.spec.js +112 -0
- package/src/commands/create.test.js +415 -0
- package/src/commands/epub.js +59 -30
- package/src/commands/epub.spec.js +114 -0
- package/src/commands/epub.test.js +503 -0
- package/src/commands/index.js +9 -2
- package/src/commands/info.js +18 -24
- package/src/commands/info.spec.js +64 -0
- package/src/commands/info.test.js +415 -0
- package/src/commands/pdf.js +58 -84
- package/src/commands/pdf.spec.js +114 -0
- package/src/commands/pdf.test.js +464 -0
- package/src/commands/preview.js +26 -31
- package/src/commands/preview.spec.js +97 -0
- package/src/commands/preview.test.js +250 -0
- package/src/commands/use.js +56 -0
- package/src/commands/use.spec.js +61 -0
- package/src/commands/use.test.js +280 -0
- package/src/commands/validate.js +31 -19
- package/src/commands/validate.spec.js +82 -0
- package/src/commands/validate.test.js +234 -0
- package/src/commands/workflows.js +70 -0
- package/src/errors/build/build-failed-error.js +19 -0
- package/src/errors/build/config-field-missing-error.js +20 -0
- package/src/errors/build/config-file-not-found-error.js +20 -0
- package/src/errors/build/index.js +11 -0
- package/src/errors/index.js +48 -0
- package/src/errors/install/dependency-install-error.js +19 -0
- package/src/errors/install/directory-not-empty-error.js +25 -0
- package/src/errors/install/index.js +12 -0
- package/src/errors/install/invalid-path-error.js +31 -0
- package/src/errors/install/invalid-starter-error.js +27 -0
- package/src/errors/install/version-not-found-error.js +35 -0
- package/src/errors/output/epub-generation-error.js +19 -0
- package/src/errors/output/index.js +14 -0
- package/src/errors/output/invalid-epub-library-error.js +20 -0
- package/src/errors/output/invalid-pdf-library-error.js +20 -0
- package/src/errors/output/missing-build-output-error.js +21 -0
- package/src/errors/output/pdf-generation-error.js +24 -0
- package/src/errors/output/tool-not-found-error.js +37 -0
- package/src/errors/project/index.js +10 -0
- package/src/errors/project/not-in-project-error.js +20 -0
- package/src/errors/project/project-create-error.js +21 -0
- package/src/errors/quire-error.js +27 -0
- package/src/errors/validation/validation-error.js +20 -11
- package/src/helpers/clean.js +1 -1
- package/src/helpers/docs-url.js +32 -0
- package/src/helpers/test-cwd.js +5 -6
- package/src/helpers/test-cwd.test.js +192 -0
- package/src/helpers/which.js +10 -4
- package/src/lib/11ty/README.md +135 -19
- package/src/lib/11ty/api.js +176 -93
- package/src/lib/11ty/cli.js +77 -35
- package/src/lib/11ty/index.js +64 -5
- package/src/lib/11ty/index.test.js +655 -0
- package/src/lib/README.md +275 -0
- package/src/lib/commander/index.js +100 -0
- package/src/lib/commander/index.test.js +86 -0
- package/src/lib/commander/options.js +195 -0
- package/src/lib/commander/options.test.js +109 -0
- package/src/lib/conf/README.md +84 -73
- package/src/lib/conf/config.js +5 -3
- package/src/lib/conf/config.test.js +281 -0
- package/src/lib/conf/defaults.js +44 -0
- package/src/lib/conf/format.js +60 -0
- package/src/lib/conf/format.test.js +106 -0
- package/src/lib/conf/helpers.js +91 -0
- package/src/lib/conf/helpers.test.js +136 -0
- package/src/lib/conf/index.js +22 -0
- package/src/lib/conf/schema.js +53 -8
- package/src/lib/epub/README.md +133 -2
- package/src/lib/epub/engines.js +46 -0
- package/src/lib/epub/epub.js +36 -11
- package/src/lib/epub/index.js +111 -21
- package/src/lib/epub/index.test.js +518 -0
- package/src/lib/epub/pandoc.js +33 -4
- package/src/lib/epub/pandoc.test.js +122 -0
- package/src/lib/epub/schema.js +21 -0
- package/src/lib/error/README.md +170 -0
- package/src/lib/error/handler.js +107 -0
- package/src/lib/git/README.md +151 -2
- package/src/lib/git/index.js +217 -13
- package/src/lib/git/index.spec.js +80 -0
- package/src/lib/git/index.test.js +453 -0
- package/src/lib/installer/index.js +309 -0
- package/src/lib/installer/index.spec.js +83 -0
- package/src/lib/installer/index.test.js +545 -0
- package/src/lib/logger/README.md +424 -0
- package/src/lib/logger/debug.js +90 -0
- package/src/lib/logger/debug.spec.js +130 -0
- package/src/lib/logger/index.js +228 -0
- package/src/lib/logger/index.spec.js +131 -0
- package/src/lib/logger/index.test.js +477 -0
- package/src/lib/npm/README.md +127 -0
- package/src/lib/npm/index.js +198 -0
- package/src/lib/npm/index.spec.js +60 -0
- package/src/lib/npm/index.test.js +355 -0
- package/src/lib/pdf/README.md +131 -0
- package/src/lib/pdf/engines.js +46 -0
- package/src/lib/pdf/index.js +124 -21
- package/src/lib/pdf/index.test.js +708 -0
- package/src/lib/pdf/paged.js +100 -52
- package/src/lib/pdf/paged.test.js +366 -0
- package/src/lib/pdf/prince.js +115 -37
- package/src/lib/pdf/prince.test.js +202 -0
- package/src/lib/pdf/schema.js +21 -0
- package/src/lib/pdf/split.js +61 -33
- package/src/lib/pdf/split.test.js +445 -0
- package/src/lib/process/manager.js +110 -0
- package/src/lib/process/manager.test.js +55 -0
- package/src/lib/project/build.js +143 -0
- package/src/lib/project/build.test.js +253 -0
- package/src/lib/project/config.js +48 -0
- package/src/lib/project/config.test.js +134 -0
- package/src/{helpers/is-quire.js → lib/project/detect.js} +5 -3
- package/src/lib/project/detect.test.js +157 -0
- package/src/lib/project/index.js +40 -0
- package/src/lib/project/paths.js +224 -0
- package/src/lib/project/version.js +110 -0
- package/src/lib/project/version.test.js +350 -0
- package/src/lib/reporter/README.md +211 -2
- package/src/lib/reporter/index.js +512 -0
- package/src/lib/reporter/index.test.js +593 -0
- package/src/main.js +134 -47
- package/src/main.spec.js +61 -0
- package/src/main.test.js +347 -0
- package/src/validators/utils.js +2 -1
- package/src/commands/conf.js +0 -43
- package/src/commands/version.js +0 -43
- package/src/lib/11ty/paths.js +0 -103
- package/src/lib/i18n/README.md +0 -3
- package/src/lib/i18n/config.js +0 -53
- package/src/lib/i18n/index.js +0 -43
- package/src/lib/i18n/localeService.js +0 -58
- package/src/lib/quire/README.md +0 -19
- 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
|
+
})
|