@thegetty/quire-cli 1.0.0-rc.35 → 1.0.0-rc.37
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 +37 -36
- package/src/commands/build.spec.js +113 -0
- package/src/commands/build.test.js +397 -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 +32 -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 +196 -94
- package/src/lib/11ty/cli.js +91 -37
- 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 +132 -0
- package/src/lib/logger/debug.spec.js +130 -0
- package/src/lib/logger/debug.test.js +128 -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,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EPUB engine schema definition
|
|
3
|
+
*
|
|
4
|
+
* This module has NO imports to avoid circular dependencies.
|
|
5
|
+
* It serves as the single source of truth for EPUB engine options.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Supported EPUB engines
|
|
10
|
+
* @type {string[]}
|
|
11
|
+
*/
|
|
12
|
+
export const ENGINES = ['epubjs', 'pandoc']
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Schema definition for epubEngine config property
|
|
16
|
+
*/
|
|
17
|
+
export const schema = {
|
|
18
|
+
type: 'string',
|
|
19
|
+
enum: ENGINES,
|
|
20
|
+
description: 'Default EPUB engine to use (epubjs, pandoc)'
|
|
21
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Error Handler
|
|
2
|
+
|
|
3
|
+
Centralized error handling for the Quire CLI. All command errors flow through this handler for consistent formatting and exit code management.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
import { handleError, handleErrors, formatError } from '#lib/error/handler.js'
|
|
9
|
+
|
|
10
|
+
try {
|
|
11
|
+
await someOperation()
|
|
12
|
+
} catch (error) {
|
|
13
|
+
handleError(error, { debug: options.debug })
|
|
14
|
+
}
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## API
|
|
18
|
+
|
|
19
|
+
### `formatError(error, options)`
|
|
20
|
+
|
|
21
|
+
Formats a QuireError for user display.
|
|
22
|
+
|
|
23
|
+
**Parameters:**
|
|
24
|
+
- `error` - The error to format (QuireError or Error)
|
|
25
|
+
- `options.debug` - Include error code in output (default: `false`)
|
|
26
|
+
|
|
27
|
+
**Returns:** Formatted string with error message and metadata.
|
|
28
|
+
|
|
29
|
+
**Output format:**
|
|
30
|
+
```
|
|
31
|
+
[ERROR_CODE] Message # Code shown only in debug mode
|
|
32
|
+
File: /path/to/file # If error.filePath is set
|
|
33
|
+
Suggestion: Actionable fix # If error.suggestion is set
|
|
34
|
+
Learn more: https://docs.url # If error.docsUrl is set
|
|
35
|
+
Tip: Run with --debug for more details # If not in debug mode and showDebugHint !== false
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### `handleError(error, options)`
|
|
39
|
+
|
|
40
|
+
Handles a single error: formats it, logs it, and exits.
|
|
41
|
+
|
|
42
|
+
**Parameters:**
|
|
43
|
+
- `error` - The error to handle
|
|
44
|
+
- `options.exit` - Whether to exit process (default: `true`)
|
|
45
|
+
- `options.exitFn` - Exit function for testing (default: `process.exit`)
|
|
46
|
+
- `options.debug` - Show stack traces (default: `false`)
|
|
47
|
+
|
|
48
|
+
**Returns:** Exit code (number)
|
|
49
|
+
|
|
50
|
+
**Behavior:**
|
|
51
|
+
- QuireError: Uses structured properties (code, exitCode, suggestion, docsUrl)
|
|
52
|
+
- Other errors: Shows as "Unexpected error" with issue report link
|
|
53
|
+
|
|
54
|
+
### `handleErrors(errors, options)`
|
|
55
|
+
|
|
56
|
+
Handles multiple errors (for batch validation scenarios).
|
|
57
|
+
|
|
58
|
+
**Parameters:** Same as `handleError`, but takes array of errors.
|
|
59
|
+
|
|
60
|
+
**Returns:** Highest exit code from all errors.
|
|
61
|
+
|
|
62
|
+
## Debug Mode
|
|
63
|
+
|
|
64
|
+
When `--debug` is enabled:
|
|
65
|
+
- Error codes are shown in the header (e.g., `[TOOL_NOT_FOUND]`)
|
|
66
|
+
- Stack traces are printed after the error message
|
|
67
|
+
- The "Tip: Run with --debug" hint is suppressed
|
|
68
|
+
|
|
69
|
+
## Debug Hint Control
|
|
70
|
+
|
|
71
|
+
By default, errors display a tip suggesting the user run with `--debug` for more details. This can be disabled for errors where the fix is obvious and debug info wouldn't help.
|
|
72
|
+
|
|
73
|
+
### Disabling the hint
|
|
74
|
+
|
|
75
|
+
Set `showDebugHint: false` in the error options:
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
class NotInProjectError extends QuireError {
|
|
79
|
+
constructor(commandName) {
|
|
80
|
+
super(`Must run inside a Quire project`, {
|
|
81
|
+
code: 'NOT_IN_PROJECT',
|
|
82
|
+
suggestion: "Navigate to your project folder with 'cd your-project-name'",
|
|
83
|
+
showDebugHint: false // Fix is obvious, debug won't help
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### When to disable
|
|
90
|
+
|
|
91
|
+
Disable the hint for "simple user errors" where:
|
|
92
|
+
- The fix is immediately obvious from the message
|
|
93
|
+
- Debug output wouldn't provide additional useful information
|
|
94
|
+
- The user just needs to take a simple corrective action
|
|
95
|
+
|
|
96
|
+
**Examples of errors with hint disabled:**
|
|
97
|
+
- `NotInProjectError` - just cd to project folder
|
|
98
|
+
- `DirectoryNotEmptyError` - choose empty directory
|
|
99
|
+
- `InvalidPathError` - verify path exists
|
|
100
|
+
- `MissingBuildOutputError` - run `quire build` first
|
|
101
|
+
- `InvalidPdfLibraryError` - use valid library name
|
|
102
|
+
- `InvalidEpubLibraryError` - use valid library name
|
|
103
|
+
|
|
104
|
+
### When to keep the hint
|
|
105
|
+
|
|
106
|
+
Keep the hint (default) for errors where:
|
|
107
|
+
- The issue may be complex or environmental
|
|
108
|
+
- Stack traces or debug logging would help diagnosis
|
|
109
|
+
- The user may need to report a bug
|
|
110
|
+
|
|
111
|
+
**Examples of errors with hint enabled (default):**
|
|
112
|
+
- `BuildFailedError` - complex build issues
|
|
113
|
+
- `PdfGenerationError` - tool/process issues
|
|
114
|
+
- `EpubGenerationError` - tool/process issues
|
|
115
|
+
- `ToolNotFoundError` - PATH/environment issues
|
|
116
|
+
- `DependencyInstallError` - npm issues
|
|
117
|
+
- Configuration and YAML validation errors
|
|
118
|
+
|
|
119
|
+
## QuireError Properties
|
|
120
|
+
|
|
121
|
+
The handler understands these QuireError properties:
|
|
122
|
+
|
|
123
|
+
| Property | Type | Description |
|
|
124
|
+
|----------|------|-------------|
|
|
125
|
+
| `message` | string | Main error message |
|
|
126
|
+
| `code` | string | Error code (e.g., `'TOOL_NOT_FOUND'`) |
|
|
127
|
+
| `exitCode` | number | Process exit code (default: 1) |
|
|
128
|
+
| `suggestion` | string | Actionable fix for the user |
|
|
129
|
+
| `docsUrl` | string | Link to relevant documentation |
|
|
130
|
+
| `filePath` | string | Source file that caused the error |
|
|
131
|
+
| `showDebugHint` | boolean | Whether to show --debug hint (default: true) |
|
|
132
|
+
|
|
133
|
+
## Exit Codes
|
|
134
|
+
|
|
135
|
+
| Code | Category | Examples |
|
|
136
|
+
|------|----------|----------|
|
|
137
|
+
| 1 | General error | Unexpected errors |
|
|
138
|
+
| 2 | Project error | Not in project, directory not empty |
|
|
139
|
+
| 3 | Build error | Config not found, build failed |
|
|
140
|
+
| 4 | Validation error | YAML parse error, validation failed |
|
|
141
|
+
| 5 | Output error | PDF/EPUB generation failed |
|
|
142
|
+
| 6 | Install error | Invalid path, dependency install failed |
|
|
143
|
+
|
|
144
|
+
## Testing
|
|
145
|
+
|
|
146
|
+
Use the `exit` and `exitFn` options to prevent actual process exit:
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
import { handleError } from '#lib/error/handler.js'
|
|
150
|
+
|
|
151
|
+
const exitCode = handleError(error, {
|
|
152
|
+
exit: false, // Don't call process.exit
|
|
153
|
+
debug: true
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
t.is(exitCode, 5)
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Or provide a mock exit function:
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
const mockExit = sinon.stub()
|
|
163
|
+
|
|
164
|
+
handleError(error, {
|
|
165
|
+
exitFn: mockExit,
|
|
166
|
+
debug: false
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
t.true(mockExit.calledWith(5))
|
|
170
|
+
```
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized error handler for the Quire CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent error formatting and exit code management.
|
|
5
|
+
* All command errors should flow through this handler.
|
|
6
|
+
*
|
|
7
|
+
* @module lib/error/handler
|
|
8
|
+
*/
|
|
9
|
+
import { logger } from '#lib/logger/index.js'
|
|
10
|
+
import QuireError from '#src/errors/quire-error.js'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Format a QuireError for user display
|
|
14
|
+
*
|
|
15
|
+
* @param {QuireError} error - The error to format
|
|
16
|
+
* @param {Object} options - Formatting options
|
|
17
|
+
* @param {boolean} options.debug - Include error code in output (default: false)
|
|
18
|
+
* @returns {string} Formatted error message
|
|
19
|
+
*/
|
|
20
|
+
export function formatError(error, options = {}) {
|
|
21
|
+
const { debug = false } = options
|
|
22
|
+
// Only show error code (e.g., INVALID_PATH) in debug mode
|
|
23
|
+
const header = (debug && error.code) ? `${error.code} ${error.message}` : error.message
|
|
24
|
+
const lines = [header]
|
|
25
|
+
if (error.filePath) lines.push(` File: ${error.filePath}`)
|
|
26
|
+
if (error.suggestion) lines.push(` Suggestion: ${error.suggestion}`)
|
|
27
|
+
if (error.docsUrl) lines.push(` Learn more: ${error.docsUrl}`)
|
|
28
|
+
// Show debug hint when not in debug mode and error opts in
|
|
29
|
+
if (!debug && error.showDebugHint !== false) {
|
|
30
|
+
lines.push(' Tip: Run with --debug for more details')
|
|
31
|
+
}
|
|
32
|
+
return lines.join('\n')
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Handle a single error
|
|
37
|
+
*
|
|
38
|
+
* @param {Error} error - The error to handle
|
|
39
|
+
* @param {Object} options
|
|
40
|
+
* @param {boolean} options.exit - Whether to exit process (default: true)
|
|
41
|
+
* @param {Function} options.exitFn - Exit function for testing (default: process.exit)
|
|
42
|
+
* @param {boolean} options.debug - Show stack traces (default: false)
|
|
43
|
+
* @returns {number} Exit code
|
|
44
|
+
*/
|
|
45
|
+
export function handleError(error, options = {}) {
|
|
46
|
+
const { exit = true, exitFn = process.exit, debug = false } = options
|
|
47
|
+
|
|
48
|
+
if (error instanceof QuireError) {
|
|
49
|
+
logger.error(formatError(error, { debug }))
|
|
50
|
+
|
|
51
|
+
if (debug && error.stack) {
|
|
52
|
+
logger.debug('Stack trace:')
|
|
53
|
+
logger.debug(error.stack)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (exit) exitFn(error.exitCode)
|
|
57
|
+
return error.exitCode
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Non-Quire errors (unexpected)
|
|
61
|
+
logger.error(`Unexpected error: ${error.message}`)
|
|
62
|
+
logger.info('Please report this issue: https://github.com/thegetty/quire/issues')
|
|
63
|
+
|
|
64
|
+
if (debug && error.stack) {
|
|
65
|
+
logger.debug('Stack trace:')
|
|
66
|
+
logger.debug(error.stack)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (exit) exitFn(1)
|
|
70
|
+
return 1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Handle multiple errors (for batch validation)
|
|
75
|
+
*
|
|
76
|
+
* @param {Error[]} errors - Array of errors
|
|
77
|
+
* @param {Object} options - Same as handleError
|
|
78
|
+
* @returns {number} Highest exit code
|
|
79
|
+
*/
|
|
80
|
+
export function handleErrors(errors, options = {}) {
|
|
81
|
+
if (!errors.length) return 0
|
|
82
|
+
|
|
83
|
+
const { exit = true, exitFn = process.exit, debug = false } = options
|
|
84
|
+
|
|
85
|
+
logger.error(`${errors.length} error(s) occurred:\n`)
|
|
86
|
+
|
|
87
|
+
let maxExitCode = 1
|
|
88
|
+
errors.forEach((error, i) => {
|
|
89
|
+
const formatted = error instanceof QuireError ? formatError(error, { debug }) : error.message
|
|
90
|
+
logger.error(`${i + 1}. ${formatted}`)
|
|
91
|
+
if (error instanceof QuireError && error.exitCode > maxExitCode) {
|
|
92
|
+
maxExitCode = error.exitCode
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (debug) {
|
|
97
|
+
errors.forEach((error) => {
|
|
98
|
+
if (error.stack) {
|
|
99
|
+
logger.debug(`Stack trace for ${error.message}:`)
|
|
100
|
+
logger.debug(error.stack)
|
|
101
|
+
}
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (exit) exitFn(maxExitCode)
|
|
106
|
+
return maxExitCode
|
|
107
|
+
}
|
package/src/lib/git/README.md
CHANGED
|
@@ -1,3 +1,152 @@
|
|
|
1
|
-
|
|
1
|
+
# Git Façade
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A façade module that abstracts git command-line operations for the Quire CLI.
|
|
4
|
+
|
|
5
|
+
## Purpose
|
|
6
|
+
|
|
7
|
+
This module provides a unified interface for git operations with:
|
|
8
|
+
|
|
9
|
+
- Encapsulation of git implementation details
|
|
10
|
+
- Consistent logging prefixed with `[CLI:lib/git]`
|
|
11
|
+
- Unified error handling
|
|
12
|
+
- Easy mockability for testing
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
### Singleton (for global operations)
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
import git from '#lib/git/index.js'
|
|
20
|
+
|
|
21
|
+
// Check git availability
|
|
22
|
+
if (!git.isAvailable()) {
|
|
23
|
+
console.error('git is not installed')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Get git version
|
|
27
|
+
// @see https://git-scm.com/docs/git-version
|
|
28
|
+
const version = await git.version()
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Git Class (for repository-scoped operations)
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
import { Git } from '#lib/git/index.js'
|
|
35
|
+
|
|
36
|
+
// Create instance with working directory
|
|
37
|
+
const repo = new Git('/path/to/project')
|
|
38
|
+
|
|
39
|
+
// Clone a repository
|
|
40
|
+
// @see https://git-scm.com/docs/git-clone
|
|
41
|
+
await repo.clone('https://github.com/user/repo', '.')
|
|
42
|
+
|
|
43
|
+
// Initialize a repository
|
|
44
|
+
// @see https://git-scm.com/docs/git-init
|
|
45
|
+
await repo.init()
|
|
46
|
+
|
|
47
|
+
// Stage files
|
|
48
|
+
// @see https://git-scm.com/docs/git-add
|
|
49
|
+
await repo.add('.')
|
|
50
|
+
await repo.add(['file1.js', 'file2.js'])
|
|
51
|
+
|
|
52
|
+
// Create a commit
|
|
53
|
+
// @see https://git-scm.com/docs/git-commit
|
|
54
|
+
await repo.commit('Initial commit')
|
|
55
|
+
|
|
56
|
+
// Remove files from tracking
|
|
57
|
+
// @see https://git-scm.com/docs/git-rm
|
|
58
|
+
await repo.rm('package.json')
|
|
59
|
+
await repo.rm(['file1.js', 'file2.js'])
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## API
|
|
63
|
+
|
|
64
|
+
### Constructor
|
|
65
|
+
|
|
66
|
+
#### `new Git(cwd)`
|
|
67
|
+
|
|
68
|
+
Create a Git façade instance scoped to a working directory.
|
|
69
|
+
|
|
70
|
+
**Parameters:**
|
|
71
|
+
- `cwd` (string, optional) - Working directory for all operations
|
|
72
|
+
|
|
73
|
+
### Methods
|
|
74
|
+
|
|
75
|
+
#### `isAvailable()`
|
|
76
|
+
|
|
77
|
+
Check if git is available in PATH.
|
|
78
|
+
|
|
79
|
+
#### `version()`
|
|
80
|
+
|
|
81
|
+
Get the installed git version.
|
|
82
|
+
|
|
83
|
+
#### `add(files)`
|
|
84
|
+
|
|
85
|
+
Stage files for commit.
|
|
86
|
+
|
|
87
|
+
**Parameters:**
|
|
88
|
+
- `files` (string|string[]) - Files to stage (use `'.'` for all)
|
|
89
|
+
|
|
90
|
+
#### `clone(url, destination)`
|
|
91
|
+
|
|
92
|
+
Clone a repository.
|
|
93
|
+
|
|
94
|
+
**Parameters:**
|
|
95
|
+
- `url` (string) - Repository URL
|
|
96
|
+
- `destination` (string, default: `'.'`) - Destination directory
|
|
97
|
+
|
|
98
|
+
#### `commit(message)`
|
|
99
|
+
|
|
100
|
+
Create a commit with staged changes.
|
|
101
|
+
|
|
102
|
+
**Parameters:**
|
|
103
|
+
- `message` (string) - Commit message
|
|
104
|
+
|
|
105
|
+
#### `init()`
|
|
106
|
+
|
|
107
|
+
Initialize a new git repository.
|
|
108
|
+
|
|
109
|
+
#### `rm(files)`
|
|
110
|
+
|
|
111
|
+
Remove files from the working tree and index.
|
|
112
|
+
|
|
113
|
+
**Parameters:**
|
|
114
|
+
- `files` (string|string[]) - Files to remove
|
|
115
|
+
|
|
116
|
+
## Testing
|
|
117
|
+
|
|
118
|
+
### Mocking the singleton
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
import esmock from 'esmock'
|
|
122
|
+
|
|
123
|
+
const mockGit = {
|
|
124
|
+
isAvailable: sandbox.stub().returns(true),
|
|
125
|
+
version: sandbox.stub().resolves('2.39.0'),
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const MyCommand = await esmock('./mycommand.js', {
|
|
129
|
+
'#lib/git/index.js': { default: mockGit }
|
|
130
|
+
})
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Mocking the Git class
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
import esmock from 'esmock'
|
|
137
|
+
|
|
138
|
+
const MockGit = class {
|
|
139
|
+
constructor(cwd) {
|
|
140
|
+
this.cwd = cwd
|
|
141
|
+
this.add = sandbox.stub().resolves()
|
|
142
|
+
this.clone = sandbox.stub().resolves()
|
|
143
|
+
this.commit = sandbox.stub().resolves()
|
|
144
|
+
this.init = sandbox.stub().resolves()
|
|
145
|
+
this.rm = sandbox.stub().resolves()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const MyCommand = await esmock('./mycommand.js', {
|
|
150
|
+
'#lib/git/index.js': { Git: MockGit }
|
|
151
|
+
})
|
|
152
|
+
```
|
package/src/lib/git/index.js
CHANGED
|
@@ -1,22 +1,226 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Git façade providing abstracted git operations
|
|
3
|
+
*
|
|
4
|
+
* Provides a consistent interface for git commands with unified
|
|
5
|
+
* logging, error handling, and testability.
|
|
6
|
+
*
|
|
7
|
+
* @example using default singleton for global operations
|
|
8
|
+
* import git from '#lib/git/index.js'
|
|
9
|
+
* const version = await git.version()
|
|
10
|
+
* if (!git.isAvailable()) { ... }
|
|
11
|
+
*
|
|
12
|
+
* @example using Git class for repository-scoped operations
|
|
13
|
+
* import { Git } from '#lib/git/index.js'
|
|
14
|
+
* const repo = new Git('/path/to/project')
|
|
15
|
+
* await repo.init()
|
|
16
|
+
* await repo.add('.')
|
|
17
|
+
* await repo.commit('Initial commit')
|
|
18
|
+
*
|
|
19
|
+
* @example mocking in tests
|
|
20
|
+
* const mockGit = {
|
|
21
|
+
* clone: sandbox.stub().resolves(),
|
|
22
|
+
* commit: sandbox.stub().resolves(),
|
|
23
|
+
* }
|
|
24
|
+
* const MyCommand = await esmock('./mycommand.js', {
|
|
25
|
+
* '#lib/git/index.js': { default: mockGit }
|
|
26
|
+
* })
|
|
27
|
+
*
|
|
28
|
+
* @see https://git-scm.com/docs - git documentation
|
|
29
|
+
* @module git
|
|
30
|
+
*/
|
|
31
|
+
import { execa } from 'execa'
|
|
32
|
+
import fs from 'node:fs'
|
|
33
|
+
import path from 'node:path'
|
|
34
|
+
import which from '#helpers/which.js'
|
|
35
|
+
import createDebug from '#debug'
|
|
36
|
+
|
|
37
|
+
const debug = createDebug('lib:git')
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if a source string is a remote URL (vs local path)
|
|
41
|
+
*
|
|
42
|
+
* Git supports various remote URL formats:
|
|
43
|
+
* - https://github.com/user/repo.git
|
|
44
|
+
* - git@github.com:user/repo.git
|
|
45
|
+
* - ssh://git@github.com/user/repo.git
|
|
46
|
+
* - http://github.com/user/repo.git
|
|
47
|
+
* - git://github.com/user/repo.git
|
|
48
|
+
*
|
|
49
|
+
* @param {string} source - Source string to check
|
|
50
|
+
* @returns {boolean} True if source appears to be a remote URL
|
|
51
|
+
*/
|
|
52
|
+
export function isRemoteUrl(source) {
|
|
53
|
+
return source.startsWith('https://') ||
|
|
54
|
+
source.startsWith('git@') ||
|
|
55
|
+
source.startsWith('ssh://') ||
|
|
56
|
+
source.startsWith('http://') ||
|
|
57
|
+
source.startsWith('git://')
|
|
58
|
+
}
|
|
2
59
|
|
|
3
60
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
61
|
+
* Check if a path is a git repository
|
|
62
|
+
*
|
|
63
|
+
* A directory is considered a git repository if it contains a .git directory.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} dirPath - Path to check
|
|
66
|
+
* @returns {boolean} True if the path is a git repository
|
|
6
67
|
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
maxConcurrentProcesses: 6,
|
|
11
|
-
trimmed: false,
|
|
68
|
+
export function isGitRepository(dirPath) {
|
|
69
|
+
const gitDir = path.join(dirPath, '.git')
|
|
70
|
+
return fs.existsSync(gitDir)
|
|
12
71
|
}
|
|
13
72
|
|
|
14
73
|
/**
|
|
15
|
-
*
|
|
16
|
-
* @see https://github.com/steveukx/git-js#api
|
|
74
|
+
* Validate a clone source before attempting to clone
|
|
17
75
|
*
|
|
18
|
-
*
|
|
76
|
+
* For remote URLs, validation is skipped (let git clone handle network errors).
|
|
77
|
+
* For local paths, validates the path exists and is a git repository.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} source - Clone source (URL or local path)
|
|
80
|
+
* @returns {{ valid: boolean, reason?: string }} Validation result
|
|
19
81
|
*/
|
|
20
|
-
|
|
82
|
+
export function validateCloneSource(source) {
|
|
83
|
+
// Remote URLs are assumed valid - let git clone handle errors
|
|
84
|
+
if (isRemoteUrl(source)) {
|
|
85
|
+
return { valid: true }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Local path validation
|
|
89
|
+
if (!fs.existsSync(source)) {
|
|
90
|
+
return { valid: false, reason: 'path does not exist' }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!isGitRepository(source)) {
|
|
94
|
+
return { valid: false, reason: 'not a git repository' }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return { valid: true }
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Git façade class
|
|
102
|
+
*/
|
|
103
|
+
class Git {
|
|
104
|
+
/**
|
|
105
|
+
* Create a Git façade instance
|
|
106
|
+
* @param {string} [cwd] - Working directory for all operations
|
|
107
|
+
*/
|
|
108
|
+
constructor(cwd) {
|
|
109
|
+
this.cwd = cwd
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get execa options with working directory
|
|
114
|
+
* @private
|
|
115
|
+
* @returns {Object} Options object for execa
|
|
116
|
+
*/
|
|
117
|
+
#getOptions() {
|
|
118
|
+
return this.cwd ? { cwd: this.cwd } : {}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Get resolved working directory for logging
|
|
123
|
+
* @private
|
|
124
|
+
* @returns {string} Resolved absolute path
|
|
125
|
+
*/
|
|
126
|
+
#resolvedCwd() {
|
|
127
|
+
return path.resolve(this.cwd || process.cwd())
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Stage files for commit
|
|
132
|
+
* @see https://git-scm.com/docs/git-add
|
|
133
|
+
* @param {string|string[]} files - Files to stage (use '.' for all)
|
|
134
|
+
* @returns {Promise<void>}
|
|
135
|
+
*/
|
|
136
|
+
async add(files) {
|
|
137
|
+
const fileList = Array.isArray(files) ? files : [files]
|
|
138
|
+
debug('staging files in %s: %s', this.#resolvedCwd(), fileList.join(', '))
|
|
139
|
+
const { stderr } = await execa('git', ['add', ...fileList], this.#getOptions())
|
|
140
|
+
if (stderr) {
|
|
141
|
+
debug('git add stderr: %s', stderr)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Clone a repository
|
|
147
|
+
* @see https://git-scm.com/docs/git-clone
|
|
148
|
+
* @param {string} url - Repository URL
|
|
149
|
+
* @param {string} [destination='.'] - Destination directory
|
|
150
|
+
* @returns {Promise<void>}
|
|
151
|
+
*/
|
|
152
|
+
async clone(url, destination = '.') {
|
|
153
|
+
const resolvedDest = path.resolve(this.#resolvedCwd(), destination)
|
|
154
|
+
debug('cloning %s to %s', url, resolvedDest)
|
|
155
|
+
const { stderr } = await execa('git', ['clone', url, destination], this.#getOptions())
|
|
156
|
+
if (stderr) {
|
|
157
|
+
debug('git clone stderr: %s', stderr)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Create a commit
|
|
163
|
+
* @see https://git-scm.com/docs/git-commit
|
|
164
|
+
* @param {string} message - Commit message
|
|
165
|
+
* @returns {Promise<void>}
|
|
166
|
+
*/
|
|
167
|
+
async commit(message) {
|
|
168
|
+
debug('committing in %s: %s...', this.#resolvedCwd(), message.substring(0, 50))
|
|
169
|
+
const { stderr } = await execa('git', ['commit', '-m', message], this.#getOptions())
|
|
170
|
+
if (stderr) {
|
|
171
|
+
debug('git commit stderr: %s', stderr)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Initialize a new repository
|
|
177
|
+
* @see https://git-scm.com/docs/git-init
|
|
178
|
+
* @returns {Promise<void>}
|
|
179
|
+
*/
|
|
180
|
+
async init() {
|
|
181
|
+
debug('initializing repository in %s', this.#resolvedCwd())
|
|
182
|
+
const { stderr } = await execa('git', ['init'], this.#getOptions())
|
|
183
|
+
if (stderr) {
|
|
184
|
+
debug('git init stderr: %s', stderr)
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if git is available in PATH
|
|
190
|
+
* @returns {boolean} True if git is available
|
|
191
|
+
*/
|
|
192
|
+
isAvailable() {
|
|
193
|
+
return !!which('git')
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Remove files from the working tree and index
|
|
198
|
+
* @see https://git-scm.com/docs/git-rm
|
|
199
|
+
* @param {string|string[]} files - Files to remove
|
|
200
|
+
* @returns {Promise<void>}
|
|
201
|
+
*/
|
|
202
|
+
async rm(files) {
|
|
203
|
+
const fileList = Array.isArray(files) ? files : [files]
|
|
204
|
+
debug('removing files in %s: %s', this.#resolvedCwd(), fileList.join(', '))
|
|
205
|
+
const { stderr } = await execa('git', ['rm', ...fileList], this.#getOptions())
|
|
206
|
+
if (stderr) {
|
|
207
|
+
debug('git rm stderr: %s', stderr)
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get git version
|
|
213
|
+
* @see https://git-scm.com/docs/git-version
|
|
214
|
+
* @returns {Promise<string>} git version string
|
|
215
|
+
*/
|
|
216
|
+
async version() {
|
|
217
|
+
const { stdout } = await execa('git', ['--version'])
|
|
218
|
+
return stdout.replace('git version ', '')
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Export class for repository-scoped operations
|
|
223
|
+
export { Git }
|
|
21
224
|
|
|
22
|
-
|
|
225
|
+
// Export singleton instance for global operations
|
|
226
|
+
export default new Git()
|