@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.
- package/CHANGELOG.md +36 -0
- package/README.md +9 -0
- package/bin/cli.js +19 -1
- package/package.json +21 -8
- package/schemas/config.schema.json +194 -0
- package/schemas/figures.schema.json +56 -0
- package/schemas/layout.schema.json +5 -0
- package/schemas/objects.schema.json +62 -0
- package/schemas/publication.schema.json +140 -0
- package/schemas/references.schema.json +29 -0
- 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,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
|
+
}
|