git-er-done 0.1.11 → 0.1.13

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/README.md CHANGED
@@ -147,6 +147,39 @@ if (mdFiles.edited) {
147
147
  }
148
148
  ```
149
149
 
150
+ ### Getting File Creation and Modification Dates
151
+
152
+ Get git timestamps for when files were created and last modified:
153
+
154
+ ```js
155
+ const { getFileDates, getFileModifiedTimeStamp, getFileCreatedTimeStamp } = require('git-er-done')
156
+
157
+ // Get both dates efficiently (recommended)
158
+ const dates = await getFileDates('src/index.js')
159
+ console.log('Created:', dates.createdDate)
160
+ console.log('Modified:', dates.modifiedDate)
161
+ console.log('Created timestamp:', dates.created) // Unix timestamp in seconds
162
+ console.log('Modified timestamp:', dates.modified) // Unix timestamp in seconds
163
+
164
+ // Or get just modified date
165
+ const modifiedTimestamp = await getFileModifiedTimeStamp('README.md')
166
+ console.log('Last modified:', new Date(modifiedTimestamp * 1000))
167
+
168
+ // Or get just created date
169
+ const createdTimestamp = await getFileCreatedTimeStamp('README.md')
170
+ console.log('First committed:', new Date(createdTimestamp * 1000))
171
+
172
+ // Get dates for multiple files (pass an array)
173
+ const files = ['README.md', 'package.json', 'src/index.js']
174
+ const fileDates = await getFileDates(files)
175
+
176
+ for (const [file, info] of Object.entries(fileDates)) {
177
+ if (!info.error) {
178
+ console.log(`${file}: last modified ${info.modifiedDate.toISOString()}`)
179
+ }
180
+ }
181
+ ```
182
+
150
183
  ### Getting Detailed File Information
151
184
 
152
185
  The `fileMatch` function returns detailed information about matched files:
@@ -168,6 +201,70 @@ console.log('All edited test files:', testFiles.editedFiles)
168
201
  if (testFiles.edited) {
169
202
  console.log('Tests have been modified - run test suite')
170
203
  }
204
+ ```
205
+
206
+ ### Getting Git Root Directory
207
+
208
+ ```js
209
+ const { getGitRoot } = require('git-er-done')
210
+
211
+ const root = await getGitRoot()
212
+ console.log('Git root:', root) // '/Users/you/your-repo'
213
+ ```
214
+
215
+ ### Getting File Contents at a Specific Commit
216
+
217
+ Retrieve the contents of a file as it existed at a specific commit:
218
+
219
+ ```js
220
+ const { getFileAtCommit } = require('git-er-done')
221
+
222
+ // Get file contents from previous commit
223
+ const previousVersion = await getFileAtCommit('src/index.js', 'HEAD~1')
224
+ console.log('Previous version:', previousVersion)
225
+
226
+ // Get file at specific SHA
227
+ const oldVersion = await getFileAtCommit('package.json', 'abc123')
228
+
229
+ // With cwd option for running in a different directory
230
+ const contents = await getFileAtCommit('src/app.js', 'main', {
231
+ cwd: '/path/to/other/repo'
232
+ })
233
+ ```
234
+
235
+ ### Getting Git Remotes
236
+
237
+ ```js
238
+ const { getRemotes, getRemote } = require('git-er-done')
239
+
240
+ // Get all remotes
241
+ const remotes = await getRemotes()
242
+ console.log('All remotes:', Object.keys(remotes)) // ['origin', 'upstream']
243
+ console.log('Origin URL:', remotes.origin.url)
244
+
245
+ // Get a specific remote
246
+ const origin = await getRemote('origin')
247
+ console.log('Origin:', origin)
248
+ // { name: 'origin', url: 'git@github.com:user/repo.git', fetchUrl: '...', pushUrl: '...' }
249
+ ```
250
+
251
+ ### Subpath Exports
252
+
253
+ Import only what you need for smaller bundles:
254
+
255
+ ```js
256
+ // Individual imports
257
+ const { getCommit } = require('git-er-done/get-commit')
258
+ const { getAllCommits } = require('git-er-done/get-all-commits')
259
+ const { getFirstCommit } = require('git-er-done/get-first-commit')
260
+ const { getLastCommit } = require('git-er-done/get-last-commit')
261
+ const { gitDetails } = require('git-er-done/get-details')
262
+ const { getGitRoot } = require('git-er-done/get-root')
263
+ const { getGitFiles } = require('git-er-done/get-files')
264
+ const { getFileAtCommit } = require('git-er-done/get-file-at-commit')
265
+ const { getFileDates, getFileModifiedTimeStamp, getFileCreatedTimeStamp } = require('git-er-done/get-file-dates')
266
+ const { getRemotes, getRemote } = require('git-er-done/get-remotes')
267
+ ```
171
268
 
172
269
  ## Examples
173
270
 
@@ -178,6 +275,7 @@ Check out the [`examples`](./examples) directory for more use cases:
178
275
  - [`get-all-commits.js`](./examples/get-all-commits.js) - Retrieve and display all commits
179
276
  - [`get-git-files.js`](./examples/get-git-files.js) - Get list of all git-tracked files
180
277
  - [`get-specific-commit-info.js`](./examples/get-specific-commit-info.js) - Get detailed information about a specific commit
278
+ - [`get-file-dates.js`](./examples/get-file-dates.js) - Get creation and modification dates for files
181
279
 
182
280
  ### File Change Detection
183
281
  - [`detect-file-changes.js`](./examples/detect-file-changes.js) - Detect specific file changes between commits
@@ -191,6 +289,7 @@ Check out the [`examples`](./examples) directory for more use cases:
191
289
  - [`check-config-changes.js`](./examples/check-config-changes.js) - Detect configuration file changes for CI/CD pipelines
192
290
  - [`code-review-helper.js`](./examples/code-review-helper.js) - Automated code review checklist and suggestions
193
291
  - [`monorepo-package-detection.js`](./examples/monorepo-package-detection.js) - Detect which packages changed in a monorepo
292
+ - [`serverless-monorepo-detection.js`](./examples/serverless-monorepo-detection.js) - Detect changes in serverless projects within a monorepo
194
293
  - [`generate-release-notes.js`](./examples/generate-release-notes.js) - Auto-generate release notes from commits
195
294
 
196
295
  Run any example:
@@ -199,6 +298,7 @@ Run any example:
199
298
  node examples/get-git-data.js
200
299
  node examples/code-review-helper.js
201
300
  node examples/monorepo-package-detection.js
301
+ node examples/serverless-monorepo-detection.js
202
302
  ```
203
303
 
204
304
  ## Prior art
package/package.json CHANGED
@@ -1,9 +1,59 @@
1
1
  {
2
2
  "name": "git-er-done",
3
- "version": "0.1.11",
3
+ "version": "0.1.13",
4
4
  "description": "Utility for dealing with modified, created, deleted files since a git commit",
5
5
  "main": "src/index.js",
6
6
  "types": "types/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./types/index.d.ts",
10
+ "default": "./src/index.js"
11
+ },
12
+ "./get-commit": {
13
+ "types": "./types/git/commits/getCommit.d.ts",
14
+ "default": "./src/git/commits/getCommit.js"
15
+ },
16
+ "./get-all-commits": {
17
+ "types": "./types/git/commits/getAllCommits.d.ts",
18
+ "default": "./src/git/commits/getAllCommits.js"
19
+ },
20
+ "./get-first-commit": {
21
+ "types": "./types/git/commits/getFirstCommit.d.ts",
22
+ "default": "./src/git/commits/getFirstCommit.js"
23
+ },
24
+ "./get-last-commit": {
25
+ "types": "./types/git/commits/getLastCommit.d.ts",
26
+ "default": "./src/git/commits/getLastCommit.js"
27
+ },
28
+ "./get-details": {
29
+ "types": "./types/git/getDetails.d.ts",
30
+ "default": "./src/git/getDetails.js"
31
+ },
32
+ "./get-root": {
33
+ "types": "./types/git/getGitRoot.d.ts",
34
+ "default": "./src/git/getGitRoot.js"
35
+ },
36
+ "./get-files": {
37
+ "types": "./types/git/getGitFiles.d.ts",
38
+ "default": "./src/git/getGitFiles.js"
39
+ },
40
+ "./get-file-at-commit": {
41
+ "types": "./types/git/getFileAtCommit.d.ts",
42
+ "default": "./src/git/getFileAtCommit.js"
43
+ },
44
+ "./get-diff-formatted": {
45
+ "types": "./types/git/getDiffFormatted.d.ts",
46
+ "default": "./src/git/getDiffFormatted.js"
47
+ },
48
+ "./get-file-dates": {
49
+ "types": "./types/git/dates/getFileDates.d.ts",
50
+ "default": "./src/git/dates/getFileDates.js"
51
+ },
52
+ "./get-remotes": {
53
+ "types": "./types/git/remotes/getRemotes.d.ts",
54
+ "default": "./src/git/remotes/getRemotes.js"
55
+ }
56
+ },
7
57
  "scripts": {
8
58
  "test": "uvu . \\.test\\.js$",
9
59
  "check": "tsc --noEmit",
@@ -40,7 +90,11 @@
40
90
  "url": "https://github.com/DavidWells/components/tree/master/packages/util-git-info"
41
91
  },
42
92
  "devDependencies": {
93
+ "@davidwells/extract-deps": "^0.0.5",
94
+ "@davidwells/git-split-diffs": "^2.2.1",
43
95
  "@types/node": "^22.10.1",
96
+ "configorama": "^0.6.14",
97
+ "minimatch": "^10.0.1",
44
98
  "typescript": "^5.7.2",
45
99
  "uvu": "^0.5.6"
46
100
  }
@@ -0,0 +1,204 @@
1
+ const { executeCommand } = require('../utils/exec')
2
+ const path = require('path')
3
+ const { getGitRoot } = require('../getGitRoot')
4
+ const { validateFilePath } = require('../utils/validateFilePath')
5
+
6
+ /**
7
+ * @typedef {Object} FileDateInfo
8
+ * @property {number} created - Unix timestamp (seconds) of when the file was first committed
9
+ * @property {number} modified - Unix timestamp (seconds) of when the file was last modified
10
+ * @property {Date} createdDate - JavaScript Date object of creation time
11
+ * @property {Date} modifiedDate - JavaScript Date object of last modification
12
+ */
13
+
14
+ /**
15
+ * @typedef {Object} FileDateError
16
+ * @property {string} error - Error message when file date retrieval fails
17
+ */
18
+
19
+ /**
20
+ * Gets the last modification date of a file from git history
21
+ * @param {string} filePath - Path to the file (absolute or relative to git root)
22
+ * @param {Object} [options] - Options
23
+ * @param {string} [options.cwd] - Current working directory (defaults to process.cwd())
24
+ * @returns {Promise<number>} Promise that resolves to Unix timestamp (seconds) of last modification
25
+ * @example
26
+ * const modifiedDate = await getFileModifiedTimeStamp('src/index.js')
27
+ * console.log('Last modified:', new Date(modifiedDate * 1000))
28
+ */
29
+ async function getFileModifiedTimeStamp(filePath, options = {}) {
30
+ const cwd = options.cwd || process.cwd()
31
+
32
+ return new Promise((resolve, reject) => {
33
+ // Validate file path to prevent command injection
34
+ try {
35
+ validateFilePath(filePath)
36
+ } catch (err) {
37
+ return reject(err)
38
+ }
39
+
40
+ // Get most recent commit timestamp for the file
41
+ const command = `git log -1 --pretty=format:%at --follow -- "${filePath}"`
42
+
43
+ executeCommand(command, { dst: cwd }, (err, stdout) => {
44
+ if (err) {
45
+ return reject(new Error(`Failed to get modified date for ${filePath}: ${err.message}`))
46
+ }
47
+
48
+ const timestamp = stdout.trim()
49
+ if (!timestamp || timestamp === '') {
50
+ // File might not be committed yet
51
+ return reject(new Error(`No git history found for ${filePath}`))
52
+ }
53
+
54
+ const unixTimestamp = parseInt(timestamp, 10)
55
+ if (isNaN(unixTimestamp)) {
56
+ return reject(new Error(`Invalid timestamp received for ${filePath}`))
57
+ }
58
+
59
+ resolve(unixTimestamp)
60
+ })
61
+ })
62
+ }
63
+
64
+ /**
65
+ * Gets the creation date of a file from git history (first commit)
66
+ * @param {string} filePath - Path to the file (absolute or relative to git root)
67
+ * @param {Object} [options] - Options
68
+ * @param {string} [options.cwd] - Current working directory (defaults to process.cwd())
69
+ * @returns {Promise<number>} Promise that resolves to Unix timestamp (seconds) of creation
70
+ * @example
71
+ * const createdDate = await getFileCreatedTimeStamp('src/index.js')
72
+ * console.log('Created:', new Date(createdDate * 1000))
73
+ */
74
+ async function getFileCreatedTimeStamp(filePath, options = {}) {
75
+ const cwd = options.cwd || process.cwd()
76
+
77
+ return new Promise((resolve, reject) => {
78
+ // Validate file path to prevent command injection
79
+ try {
80
+ validateFilePath(filePath)
81
+ } catch (err) {
82
+ return reject(err)
83
+ }
84
+
85
+ // Get all commit timestamps following history, then take the last one (oldest)
86
+ const command = `git log --follow --pretty=format:%at -- "${filePath}"`
87
+
88
+ executeCommand(command, { dst: cwd }, (err, stdout) => {
89
+ if (err) {
90
+ return reject(new Error(`Failed to get created date for ${filePath}: ${err.message}`))
91
+ }
92
+
93
+ const timestamps = stdout.trim().split('\n').filter(Boolean)
94
+ if (timestamps.length === 0) {
95
+ return reject(new Error(`No git history found for ${filePath}`))
96
+ }
97
+
98
+ // Last timestamp in the log is the oldest (creation)
99
+ const createdTimestamp = timestamps[timestamps.length - 1]
100
+ const unixTimestamp = parseInt(createdTimestamp, 10)
101
+
102
+ if (isNaN(unixTimestamp)) {
103
+ return reject(new Error(`Invalid timestamp received for ${filePath}`))
104
+ }
105
+
106
+ resolve(unixTimestamp)
107
+ })
108
+ })
109
+ }
110
+
111
+ /**
112
+ * Gets dates for a single file (internal helper)
113
+ * @param {string} filePath - Path to the file
114
+ * @param {Object} [options] - Options
115
+ * @param {string} [options.cwd] - Current working directory
116
+ * @returns {Promise<FileDateInfo>}
117
+ */
118
+ async function getSingleFileDates(filePath, options = {}) {
119
+ const cwd = options.cwd || process.cwd()
120
+
121
+ return new Promise((resolve, reject) => {
122
+ // Validate file path to prevent command injection
123
+ try {
124
+ validateFilePath(filePath)
125
+ } catch (err) {
126
+ return reject(err)
127
+ }
128
+
129
+ // Get all commit timestamps following history
130
+ const command = `git log --follow --pretty=format:%at -- "${filePath}"`
131
+
132
+ executeCommand(command, { dst: cwd }, (err, stdout) => {
133
+ if (err) {
134
+ return reject(new Error(`Failed to get dates for ${filePath}: ${err.message}`))
135
+ }
136
+
137
+ const timestamps = stdout.trim().split('\n').filter(Boolean)
138
+ if (timestamps.length === 0) {
139
+ return reject(new Error(`No git history found for ${filePath}`))
140
+ }
141
+
142
+ // First timestamp is most recent (modified), last is oldest (created)
143
+ const modifiedTimestamp = parseInt(timestamps[0], 10)
144
+ const createdTimestamp = parseInt(timestamps[timestamps.length - 1], 10)
145
+
146
+ if (isNaN(modifiedTimestamp) || isNaN(createdTimestamp)) {
147
+ return reject(new Error(`Invalid timestamp received for ${filePath}`))
148
+ }
149
+
150
+ resolve({
151
+ created: createdTimestamp,
152
+ modified: modifiedTimestamp,
153
+ createdDate: new Date(createdTimestamp * 1000),
154
+ modifiedDate: new Date(modifiedTimestamp * 1000)
155
+ })
156
+ })
157
+ })
158
+ }
159
+
160
+ /**
161
+ * Gets both creation and modification dates for file(s) from git history
162
+ * @param {string | string[]} filePaths - Path or array of paths to file(s)
163
+ * @param {Object} [options] - Options
164
+ * @param {string} [options.cwd] - Current working directory (defaults to process.cwd())
165
+ * @returns {Promise<FileDateInfo | Object<string, FileDateInfo | FileDateError>>} Single file returns FileDateInfo, multiple files returns object keyed by path
166
+ * @example
167
+ * // Single file
168
+ * const dates = await getFileDates('src/index.js')
169
+ * console.log('Created:', dates.createdDate)
170
+ * console.log('Modified:', dates.modifiedDate)
171
+ *
172
+ * // Multiple files
173
+ * const dates = await getFileDates(['src/index.js', 'README.md'])
174
+ * console.log('Index modified:', dates['src/index.js'].modifiedDate)
175
+ */
176
+ async function getFileDates(filePaths, options = {}) {
177
+ // Single file path
178
+ if (typeof filePaths === 'string') {
179
+ return getSingleFileDates(filePaths, options)
180
+ }
181
+
182
+ // Multiple file paths
183
+ /** @type {Object<string, FileDateInfo | FileDateError>} */
184
+ const results = {}
185
+
186
+ await Promise.all(
187
+ filePaths.map(async (filePath) => {
188
+ try {
189
+ results[filePath] = await getSingleFileDates(filePath, options)
190
+ } catch (err) {
191
+ // Store error but don't fail entire operation
192
+ results[filePath] = { error: err.message }
193
+ }
194
+ })
195
+ )
196
+
197
+ return results
198
+ }
199
+
200
+ module.exports = {
201
+ getFileModifiedTimeStamp,
202
+ getFileCreatedTimeStamp,
203
+ getFileDates
204
+ }
@@ -0,0 +1,203 @@
1
+ const path = require('path')
2
+ const { test } = require('uvu')
3
+ const assert = require('uvu/assert')
4
+
5
+ const {
6
+ getFileModifiedTimeStamp,
7
+ getFileCreatedTimeStamp,
8
+ getFileDates
9
+ } = require('./getFileDates')
10
+
11
+ // Use style-guard package files for testing (they're unlikely to change)
12
+ // Note: These paths are relative to the git root
13
+ const STYLE_GUARD_INDEX = path.join(__dirname, '../../../../style-guard/index.js')
14
+ const STYLE_GUARD_README = path.join(__dirname, '../../../../style-guard/README.md')
15
+ const STYLE_GUARD_PACKAGE = path.join(__dirname, '../../../../style-guard/package.json')
16
+
17
+ // Wrapper to add timing to tests
18
+ const timedTest = (name, fn) => {
19
+ test(name, async (context) => {
20
+ const start = Date.now()
21
+ try {
22
+ await fn(context)
23
+ } finally {
24
+ const duration = Date.now() - start
25
+ console.log(` ⏱️ ${name}: ${duration}ms`)
26
+ }
27
+ })
28
+ }
29
+
30
+ timedTest('getFileModifiedTimeStamp should return a unix timestamp', async () => {
31
+ const timestamp = await getFileModifiedTimeStamp(STYLE_GUARD_INDEX)
32
+ assert.ok(Number.isInteger(timestamp))
33
+ assert.ok(timestamp > 0)
34
+ // Should be a reasonable date (after 2020)
35
+ assert.ok(timestamp > 1577836800)
36
+ })
37
+
38
+ timedTest('getFileCreatedTimeStamp should return a unix timestamp', async () => {
39
+ const timestamp = await getFileCreatedTimeStamp(STYLE_GUARD_INDEX)
40
+ assert.ok(Number.isInteger(timestamp))
41
+ assert.ok(timestamp > 0)
42
+ // Should be a reasonable date (after 2020)
43
+ assert.ok(timestamp > 1577836800)
44
+ })
45
+
46
+ timedTest('getFileDates should return both created and modified dates', async () => {
47
+ const dates = await getFileDates(STYLE_GUARD_INDEX)
48
+ console.log('getFileDates', dates)
49
+
50
+ // Check specific timestamp values
51
+ assert.is(dates.created, 1609635897)
52
+ assert.is(dates.modified, 1610136724)
53
+
54
+ // Check specific Date object values
55
+ assert.is(dates.createdDate.toISOString(), '2021-01-03T01:04:57.000Z')
56
+ assert.is(dates.modifiedDate.toISOString(), '2021-01-08T20:12:04.000Z')
57
+
58
+ // Check timestamps exist and are valid
59
+ assert.ok(Number.isInteger(dates.created))
60
+ assert.ok(Number.isInteger(dates.modified))
61
+ assert.ok(dates.created > 0)
62
+ assert.ok(dates.modified > 0)
63
+
64
+ // Check Date objects exist and are valid
65
+ assert.ok(dates.createdDate instanceof Date)
66
+ assert.ok(dates.modifiedDate instanceof Date)
67
+
68
+ // Created date should be before or equal to modified date
69
+ assert.ok(dates.created <= dates.modified)
70
+
71
+ // Date objects should match timestamps
72
+ assert.is(dates.createdDate.getTime(), dates.created * 1000)
73
+ assert.is(dates.modifiedDate.getTime(), dates.modified * 1000)
74
+ })
75
+
76
+ timedTest('getFileDates should return dates for multiple files', async () => {
77
+ const files = [STYLE_GUARD_INDEX, STYLE_GUARD_README, STYLE_GUARD_PACKAGE]
78
+ const results = await getFileDates(files)
79
+
80
+ console.log('getFileDates (multiple)', results)
81
+
82
+ // Should have results for all files
83
+ assert.equal(Object.keys(results).sort(), files.sort())
84
+
85
+ // Each file should have valid date info
86
+ for (const file of files) {
87
+ const dates = results[file]
88
+
89
+ // Skip if there was an error
90
+ if (dates.error) {
91
+ continue
92
+ }
93
+
94
+ assert.ok(Number.isInteger(dates.created))
95
+ assert.ok(Number.isInteger(dates.modified))
96
+ assert.ok(dates.createdDate instanceof Date)
97
+ assert.ok(dates.modifiedDate instanceof Date)
98
+ }
99
+ })
100
+
101
+ timedTest('should reject invalid file paths - null bytes', async () => {
102
+ try {
103
+ await getFileModifiedTimeStamp('test\0.js')
104
+ assert.unreachable('Should have thrown an error')
105
+ } catch (err) {
106
+ assert.match(err.message, /null bytes/)
107
+ }
108
+ })
109
+
110
+ timedTest('should reject invalid file paths - command injection', async () => {
111
+ try {
112
+ await getFileModifiedTimeStamp('test.js; rm -rf /')
113
+ assert.unreachable('Should have thrown an error')
114
+ } catch (err) {
115
+ assert.match(err.message, /suspicious characters/)
116
+ }
117
+ })
118
+
119
+ timedTest('should reject invalid file paths - shell metacharacters', async () => {
120
+ const dangerousPaths = [
121
+ 'test.js|cat /etc/passwd',
122
+ 'test.js`whoami`',
123
+ 'test.js$(whoami)',
124
+ 'test.js&& echo hacked',
125
+ ]
126
+
127
+ for (const badPath of dangerousPaths) {
128
+ try {
129
+ await getFileModifiedTimeStamp(badPath)
130
+ assert.unreachable(`Should have thrown an error for: ${badPath}`)
131
+ } catch (err) {
132
+ assert.match(err.message, /suspicious characters/)
133
+ }
134
+ }
135
+ })
136
+
137
+ timedTest('should reject path traversal attempts', async () => {
138
+ try {
139
+ await getFileModifiedTimeStamp('../../etc/passwd')
140
+ assert.unreachable('Should have thrown an error')
141
+ } catch (err) {
142
+ assert.match(err.message, /suspicious characters|directory traversal/)
143
+ }
144
+ })
145
+
146
+ timedTest('should reject newlines in file paths', async () => {
147
+ try {
148
+ await getFileModifiedTimeStamp('test.js\nrm -rf /')
149
+ assert.unreachable('Should have thrown an error')
150
+ } catch (err) {
151
+ assert.match(err.message, /suspicious characters/)
152
+ }
153
+ })
154
+
155
+ timedTest('should reject empty or non-string file paths', async () => {
156
+ try {
157
+ await getFileModifiedTimeStamp('')
158
+ assert.unreachable('Should have thrown an error')
159
+ } catch (err) {
160
+ assert.match(err.message, /non-empty string/)
161
+ }
162
+
163
+ try {
164
+ await getFileModifiedTimeStamp(null)
165
+ assert.unreachable('Should have thrown an error')
166
+ } catch (err) {
167
+ assert.match(err.message, /non-empty string/)
168
+ }
169
+
170
+ try {
171
+ await getFileModifiedTimeStamp(undefined)
172
+ assert.unreachable('Should have thrown an error')
173
+ } catch (err) {
174
+ assert.match(err.message, /non-empty string/)
175
+ }
176
+ })
177
+
178
+ timedTest('should handle files with spaces in names', async () => {
179
+ // This should NOT be rejected - spaces are valid in file names
180
+ // Note: This test will fail if the file doesn't exist, but that's expected
181
+ // The important thing is that it doesn't fail due to path validation
182
+ try {
183
+ await getFileModifiedTimeStamp('test file.js')
184
+ } catch (err) {
185
+ // Should fail because file doesn't exist, not because of validation
186
+ assert.match(err.message, /No git history found|Failed to get/)
187
+ }
188
+ })
189
+
190
+ timedTest('should handle relative paths correctly', async () => {
191
+ // Relative paths should work - test with the same file
192
+ const timestamp = await getFileModifiedTimeStamp(STYLE_GUARD_INDEX)
193
+ assert.ok(Number.isInteger(timestamp))
194
+ assert.ok(timestamp > 0)
195
+ })
196
+
197
+ timedTest('created date should be less than or equal to modified date', async () => {
198
+ const dates = await getFileDates(STYLE_GUARD_INDEX)
199
+ assert.ok(dates.created <= dates.modified,
200
+ `Created date (${dates.created}) should be <= modified date (${dates.modified})`)
201
+ })
202
+
203
+ test.run()
@@ -0,0 +1,142 @@
1
+ const path = require('path')
2
+ const { promisify } = require('util')
3
+ const { exec } = require('child_process')
4
+ const { getGitRoot } = require('./getGitRoot')
5
+ const terminalSize = require('./utils/term-size')
6
+ const execAsync = promisify(exec)
7
+
8
+ /**
9
+ * Strip ANSI escape codes from a string
10
+ */
11
+ function stripAnsi(str) {
12
+ return str.replace(/\x1b\[[0-9;]*m/g, '')
13
+ }
14
+
15
+ /**
16
+ * Calculate the longest line length in a diff
17
+ */
18
+ function getLongestLineLength(diff) {
19
+ const lines = diff.split('\n')
20
+ let maxLength = 0
21
+
22
+ for (const line of lines) {
23
+ // Skip diff headers
24
+ if (line.startsWith('diff --git') ||
25
+ line.startsWith('index ') ||
26
+ line.startsWith('---') ||
27
+ line.startsWith('+++')) {
28
+ continue
29
+ }
30
+
31
+ let contentToMeasure = line
32
+
33
+ // For hunk headers (@@), extract the code context after the second @@
34
+ if (line.startsWith('@@')) {
35
+ const secondAtIndex = line.indexOf('@@', 2)
36
+ if (secondAtIndex !== -1) {
37
+ contentToMeasure = line.slice(secondAtIndex + 2)
38
+ }
39
+ } else {
40
+ // Get actual content (skip the leading space/+/-)
41
+ contentToMeasure = line.slice(1)
42
+ }
43
+
44
+ const cleanContent = stripAnsi(contentToMeasure)
45
+
46
+ if (cleanContent.length > maxLength) {
47
+ maxLength = cleanContent.length
48
+ }
49
+ }
50
+
51
+ return maxLength
52
+ }
53
+
54
+ /**
55
+ * Get formatted diff for a specific file
56
+ * @param {Object} options - Options for getting formatted diff
57
+ * @param {string} options.filePath - Relative path from git root
58
+ * @param {string} options.gitRootDir - Absolute path to git root directory
59
+ * @param {string} options.baseBranch - Base branch to compare against
60
+ * @param {boolean} options.shrinkToLongestLine - Auto-calculate width based on longest line
61
+ * @param {number} options.leftMargin - Number of spaces to add to the left of each line
62
+ * @param {number} options.width - Width of the diff output (ignored if shrinkToLongestLine is true)
63
+ * @param {boolean} options.hideHeader - Remove the file path header from the diff
64
+ */
65
+ async function getFormattedDiff({
66
+ filePath,
67
+ gitRootDir,
68
+ baseBranch = 'master',
69
+ shrinkToLongestLine = false,
70
+ leftMargin = 0,
71
+ width = 140,
72
+ hideHeader = false
73
+ }) {
74
+ try {
75
+ const { formatDiff } = await import('@davidwells/git-split-diffs')
76
+
77
+ if (!gitRootDir) {
78
+ gitRootDir = await getGitRoot()
79
+ }
80
+
81
+ // Get the diff for this specific file
82
+ const { stdout: diff } = await execAsync(`git diff ${baseBranch} --ignore-cr-at-eol -- "${filePath}"`, {
83
+ cwd: gitRootDir
84
+ })
85
+
86
+ if (!diff) {
87
+ return null
88
+ }
89
+
90
+ // Calculate width if shrinkToLongestLine is enabled
91
+ let diffWidth = width
92
+ if (shrinkToLongestLine) {
93
+ const longestLine = getLongestLineLength(diff)
94
+ diffWidth = longestLine + 25
95
+ }
96
+
97
+ // TODO cache this value
98
+ const terminalWidth = terminalSize()
99
+ let maxWidth = Math.min(terminalWidth.columns, diffWidth)
100
+
101
+ if (leftMargin > 0) {
102
+ maxWidth = maxWidth - leftMargin
103
+ }
104
+
105
+ // Format the diff with git-split-diffs
106
+ let formatted = await formatDiff(diff, {
107
+ // hyperlinkFileNames: false,
108
+ // hyperlinkLineNumbers: false,
109
+ // hideFileHeader: true,
110
+ // omitHunkHeaders: true,
111
+ trimLastEmptyLine: true,
112
+ width: maxWidth,
113
+ minLineWidth: 80,
114
+ wrapLines: false,
115
+ highlightLineChanges: true,
116
+ themeName: 'dark',
117
+ gitRootDir,
118
+ })
119
+
120
+ // Remove header if hideHeader is true
121
+ if (hideHeader) {
122
+ const lines = formatted.split('\n')
123
+ // Skip first 3 lines (separator, file path with ■■, separator)
124
+ formatted = lines.slice(3).join('\n')
125
+ }
126
+
127
+ // Add left margin if specified
128
+ if (leftMargin > 0) {
129
+ const margin = ' '.repeat(leftMargin)
130
+ return formatted.split('\n').map(line => margin + line).join('\n')
131
+ }
132
+
133
+ return formatted
134
+ } catch (err) {
135
+ console.error(`Error formatting diff for ${filePath}:`, err.message)
136
+ return null
137
+ }
138
+ }
139
+
140
+ module.exports = {
141
+ getFormattedDiff
142
+ }
@@ -0,0 +1,27 @@
1
+ // Returns file contents at a specific git commit
2
+ const { localGetFileAtSHA } = require('./localGetFileAtSHA')
3
+ const { validateFilePath } = require('./utils/validateFilePath')
4
+
5
+ /**
6
+ * Gets the contents of a file at a specific git commit
7
+ * @param {string} filePath - Path to the file (relative to git root)
8
+ * @param {string} sha - Git commit SHA or ref (e.g., 'HEAD', 'main', 'abc123')
9
+ * @param {Object} [options] - Options
10
+ * @param {string} [options.cwd] - Working directory (defaults to process.cwd())
11
+ * @returns {Promise<string|undefined>} File contents at that commit, or undefined if not found
12
+ * @example
13
+ * const contents = await getFileAtCommit('src/index.js', 'HEAD~1')
14
+ * console.log('Previous version:', contents)
15
+ *
16
+ * // With cwd option
17
+ * const contents = await getFileAtCommit('package.json', 'HEAD', { cwd: '/path/to/repo' })
18
+ */
19
+ async function getFileAtCommit(filePath, sha, options = {}) {
20
+ validateFilePath(filePath)
21
+ if (!sha || typeof sha !== 'string') {
22
+ throw new Error('SHA must be a non-empty string')
23
+ }
24
+ return localGetFileAtSHA(filePath, null, sha, options)
25
+ }
26
+
27
+ module.exports = { getFileAtCommit }
@@ -1,21 +1,32 @@
1
+ // Gets file contents at a specific git SHA
1
2
  const { debug } = require('../debug')
2
3
  const { exec } = require('child_process')
3
4
 
4
5
  const d = debug('localGetFileAtSHA')
5
6
 
6
- const localGetFileAtSHA = (path, _repo, sha) => {
7
- return new Promise(resolve => {
7
+ /**
8
+ * Gets file contents at a specific git commit (internal)
9
+ * @param {string} path - File path relative to git root
10
+ * @param {string|null} _repo - Unused, kept for interface compatibility
11
+ * @param {string} sha - Git commit SHA or ref
12
+ * @param {Object} [options] - Options
13
+ * @param {string} [options.cwd] - Working directory (defaults to process.cwd())
14
+ * @returns {Promise<string|undefined>} File contents or undefined if not found
15
+ */
16
+ const localGetFileAtSHA = (path, _repo, sha, options = {}) => {
17
+ const cwd = options.cwd || process.cwd()
18
+ /** @type {Promise<string|undefined>} */
19
+ const promise = new Promise(resolve => {
8
20
  const call = `git show ${sha}:'${path}'`
9
21
  d(call)
10
- exec(call, (err, stdout, _stderr) => {
22
+ exec(call, { cwd }, (err, stdout, _stderr) => {
11
23
  if (err) {
12
- // console.error(`Could not get the file ${path} from git at ${sha}`)
13
- // console.error(err)
14
- return resolve()
24
+ return resolve(undefined)
15
25
  }
16
26
  resolve(stdout)
17
27
  })
18
28
  })
29
+ return promise
19
30
  }
20
31
 
21
32
  module.exports.localGetFileAtSHA = localGetFileAtSHA
@@ -0,0 +1 @@
1
+ // https://github.com/kpdecker/jsdiff/blob/master/test/patch/create.js
@@ -0,0 +1 @@
1
+ // https://github.com/humanwhocodes/gitignore-to-minimatch
@@ -0,0 +1,136 @@
1
+ const process = require('process')
2
+ const { execFileSync } = require('child_process')
3
+ const fs = require('fs')
4
+ const tty = require('tty')
5
+
6
+ const defaultColumns = 80
7
+ const defaultRows = 24
8
+
9
+ /**
10
+ * @typedef {Object} TerminalSize
11
+ * @property {number} columns - Number of columns in the terminal
12
+ * @property {number} rows - Number of rows in the terminal
13
+ */
14
+
15
+ /** @type {TerminalSize|undefined} */
16
+ let sizeCache
17
+
18
+ // @ts-ignore
19
+ function exec(command, arguments_, { shell, env } = {}) {
20
+ return execFileSync(command, arguments_, {
21
+ encoding: 'utf8',
22
+ stdio: ['ignore', 'pipe', 'ignore'],
23
+ timeout: 500,
24
+ shell,
25
+ env,
26
+ }).trim()
27
+ }
28
+
29
+ function create(columns, rows) {
30
+ return {
31
+ columns: Number.parseInt(columns, 10),
32
+ rows: Number.parseInt(rows, 10),
33
+ }
34
+ }
35
+
36
+ function createIfNotDefault(maybeColumns, maybeRows) {
37
+ const { columns, rows } = create(maybeColumns, maybeRows)
38
+
39
+ if (Number.isNaN(columns) || Number.isNaN(rows)) {
40
+ return
41
+ }
42
+
43
+ if (columns === defaultColumns && rows === defaultRows) {
44
+ return
45
+ }
46
+
47
+ return { columns, rows }
48
+ }
49
+
50
+ function devTty() {
51
+ try {
52
+ // eslint-disable-next-line no-bitwise
53
+ // @ts-ignore - O_EVTONLY is macOS-specific
54
+ const flags = process.platform === 'darwin' ? fs.constants.O_EVTONLY | fs.constants.O_NONBLOCK : fs.constants.O_NONBLOCK
55
+ // eslint-disable-next-line new-cap
56
+ // @ts-ignore
57
+ const { columns, rows } = tty.WriteStream(fs.openSync('/dev/tty', flags))
58
+ return { columns, rows }
59
+ } catch {}
60
+ }
61
+
62
+ // On macOS, this only returns correct values when stdout is not redirected.
63
+ function tput() {
64
+ try {
65
+ // `tput` requires the `TERM` environment variable to be set.
66
+ const columns = exec('tput', ['cols'], { env: { TERM: 'dumb', ...process.env } })
67
+ const rows = exec('tput', ['lines'], { env: { TERM: 'dumb', ...process.env } })
68
+
69
+ if (columns && rows) {
70
+ return createIfNotDefault(columns, rows)
71
+ }
72
+ } catch {}
73
+ }
74
+
75
+ // Only exists on Linux.
76
+ function resize() {
77
+ // `resize` is preferred as it works even when all file descriptors are redirected
78
+ // https://linux.die.net/man/1/resize
79
+ try {
80
+ const size = exec('resize', ['-u']).match(/\d+/g)
81
+
82
+ if (size.length === 2) {
83
+ return createIfNotDefault(size[0], size[1])
84
+ }
85
+ } catch {}
86
+ }
87
+
88
+ function terminalSize() {
89
+ // Return cached size if available for performance.
90
+ if (sizeCache) {
91
+ return sizeCache
92
+ }
93
+
94
+ const { env, stdout, stderr } = process
95
+
96
+ if (stdout?.columns && stdout?.rows) {
97
+ sizeCache = create(stdout.columns, stdout.rows)
98
+ return sizeCache
99
+ }
100
+
101
+ if (stderr?.columns && stderr?.rows) {
102
+ sizeCache = create(stderr.columns, stderr.rows)
103
+ return sizeCache
104
+ }
105
+
106
+ // These values are static, so not the first choice.
107
+ if (env.COLUMNS && env.LINES) {
108
+ sizeCache = create(env.COLUMNS, env.LINES)
109
+ return sizeCache
110
+ }
111
+
112
+ const fallback = {
113
+ columns: defaultColumns,
114
+ rows: defaultRows,
115
+ }
116
+
117
+ if (process.platform === 'win32') {
118
+ // We include `tput` for Windows users using Git Bash.
119
+ sizeCache = tput() ?? fallback
120
+ return sizeCache
121
+ }
122
+
123
+ if (process.platform === 'darwin') {
124
+ sizeCache = devTty() ?? tput() ?? fallback
125
+ return sizeCache
126
+ }
127
+
128
+ sizeCache = devTty() ?? tput() ?? resize() ?? fallback
129
+ return sizeCache
130
+ }
131
+
132
+ if (require.main === module) {
133
+ console.log(terminalSize())
134
+ }
135
+
136
+ module.exports = terminalSize
@@ -0,0 +1,44 @@
1
+ const path = require('path')
2
+
3
+ /**
4
+ * Validates and sanitizes a file path to prevent command injection
5
+ * @param {string} filePath - The file path to validate
6
+ * @returns {string} The validated file path
7
+ * @throws {Error} If the file path contains suspicious characters
8
+ */
9
+ function validateFilePath(filePath) {
10
+ if (!filePath || typeof filePath !== 'string') {
11
+ throw new Error('File path must be a non-empty string')
12
+ }
13
+
14
+ // Check for null bytes (common injection technique)
15
+ if (filePath.includes('\0')) {
16
+ throw new Error('File path contains null bytes')
17
+ }
18
+
19
+ // Check for command injection attempts
20
+ const dangerousPatterns = [
21
+ /[;&|`$(){}[\]<>]/, // Shell metacharacters
22
+ /\n|\r/, // Newlines
23
+ /\.\.\//, // Path traversal attempts (relative)
24
+ /^\.\.\\/, // Path traversal attempts (Windows)
25
+ ]
26
+
27
+ for (const pattern of dangerousPatterns) {
28
+ if (pattern.test(filePath)) {
29
+ throw new Error(`File path contains suspicious characters: ${filePath}`)
30
+ }
31
+ }
32
+
33
+ // Normalize the path to prevent traversal attacks
34
+ const normalized = path.normalize(filePath)
35
+
36
+ // Additional check after normalization for path traversal
37
+ if (normalized.includes('..')) {
38
+ throw new Error(`File path contains directory traversal: ${filePath}`)
39
+ }
40
+
41
+ return filePath
42
+ }
43
+
44
+ module.exports = { validateFilePath }
package/src/index.js CHANGED
@@ -4,12 +4,32 @@ const { getFirstCommit } = require('./git/commits/getFirstCommit')
4
4
  const { getLastCommit } = require('./git/commits/getLastCommit')
5
5
  const { getAllCommits } = require('./git/commits/getAllCommits')
6
6
  const { getGitFiles } = require('./git/getGitFiles')
7
+ const { getGitRoot } = require('./git/getGitRoot')
8
+ const { getRemotes, getRemote } = require('./git/remotes/getRemotes')
9
+ const { getFileAtCommit } = require('./git/getFileAtCommit')
10
+ const {
11
+ getFileModifiedTimeStamp,
12
+ getFileCreatedTimeStamp,
13
+ getFileDates
14
+ } = require('./git/dates/getFileDates')
7
15
 
8
16
  module.exports = {
17
+ // Get Git Details
18
+ gitDetails,
19
+ getGitRoot,
20
+ // Get Commits
9
21
  getCommit,
22
+ getAllCommits,
10
23
  getFirstCommit,
11
24
  getLastCommit,
12
- getAllCommits,
13
- gitDetails,
14
- getGitFiles
25
+ // Get Git Files
26
+ getGitFiles,
27
+ getFileAtCommit,
28
+ // Get Git File time details
29
+ getFileModifiedTimeStamp,
30
+ getFileCreatedTimeStamp,
31
+ getFileDates,
32
+ // Get Git Remotes
33
+ getRemotes,
34
+ getRemote,
15
35
  }
package/src/types.js CHANGED
@@ -47,4 +47,12 @@
47
47
  * @property {function(): Promise<number>} linesOfCode - Async function that returns total lines of code changed
48
48
  */
49
49
 
50
+ /**
51
+ * @typedef {Object} FileDateInfo
52
+ * @property {number} created - Unix timestamp (seconds) of when the file was first committed
53
+ * @property {number} modified - Unix timestamp (seconds) of when the file was last modified
54
+ * @property {Date} createdDate - JavaScript Date object of creation time
55
+ * @property {Date} modifiedDate - JavaScript Date object of last modification
56
+ */
57
+
50
58
  module.exports = {}
@@ -0,0 +1,82 @@
1
+ export type FileDateInfo = {
2
+ /**
3
+ * - Unix timestamp (seconds) of when the file was first committed
4
+ */
5
+ created: number;
6
+ /**
7
+ * - Unix timestamp (seconds) of when the file was last modified
8
+ */
9
+ modified: number;
10
+ /**
11
+ * - JavaScript Date object of creation time
12
+ */
13
+ createdDate: Date;
14
+ /**
15
+ * - JavaScript Date object of last modification
16
+ */
17
+ modifiedDate: Date;
18
+ };
19
+ export type FileDateError = {
20
+ /**
21
+ * - Error message when file date retrieval fails
22
+ */
23
+ error: string;
24
+ };
25
+ /**
26
+ * @typedef {Object} FileDateInfo
27
+ * @property {number} created - Unix timestamp (seconds) of when the file was first committed
28
+ * @property {number} modified - Unix timestamp (seconds) of when the file was last modified
29
+ * @property {Date} createdDate - JavaScript Date object of creation time
30
+ * @property {Date} modifiedDate - JavaScript Date object of last modification
31
+ */
32
+ /**
33
+ * @typedef {Object} FileDateError
34
+ * @property {string} error - Error message when file date retrieval fails
35
+ */
36
+ /**
37
+ * Gets the last modification date of a file from git history
38
+ * @param {string} filePath - Path to the file (absolute or relative to git root)
39
+ * @param {Object} [options] - Options
40
+ * @param {string} [options.cwd] - Current working directory (defaults to process.cwd())
41
+ * @returns {Promise<number>} Promise that resolves to Unix timestamp (seconds) of last modification
42
+ * @example
43
+ * const modifiedDate = await getFileModifiedTimeStamp('src/index.js')
44
+ * console.log('Last modified:', new Date(modifiedDate * 1000))
45
+ */
46
+ export function getFileModifiedTimeStamp(filePath: string, options?: {
47
+ cwd?: string;
48
+ }): Promise<number>;
49
+ /**
50
+ * Gets the creation date of a file from git history (first commit)
51
+ * @param {string} filePath - Path to the file (absolute or relative to git root)
52
+ * @param {Object} [options] - Options
53
+ * @param {string} [options.cwd] - Current working directory (defaults to process.cwd())
54
+ * @returns {Promise<number>} Promise that resolves to Unix timestamp (seconds) of creation
55
+ * @example
56
+ * const createdDate = await getFileCreatedTimeStamp('src/index.js')
57
+ * console.log('Created:', new Date(createdDate * 1000))
58
+ */
59
+ export function getFileCreatedTimeStamp(filePath: string, options?: {
60
+ cwd?: string;
61
+ }): Promise<number>;
62
+ /**
63
+ * Gets both creation and modification dates for file(s) from git history
64
+ * @param {string | string[]} filePaths - Path or array of paths to file(s)
65
+ * @param {Object} [options] - Options
66
+ * @param {string} [options.cwd] - Current working directory (defaults to process.cwd())
67
+ * @returns {Promise<FileDateInfo | Object<string, FileDateInfo | FileDateError>>} Single file returns FileDateInfo, multiple files returns object keyed by path
68
+ * @example
69
+ * // Single file
70
+ * const dates = await getFileDates('src/index.js')
71
+ * console.log('Created:', dates.createdDate)
72
+ * console.log('Modified:', dates.modifiedDate)
73
+ *
74
+ * // Multiple files
75
+ * const dates = await getFileDates(['src/index.js', 'README.md'])
76
+ * console.log('Index modified:', dates['src/index.js'].modifiedDate)
77
+ */
78
+ export function getFileDates(filePaths: string | string[], options?: {
79
+ cwd?: string;
80
+ }): Promise<FileDateInfo | {
81
+ [x: string]: FileDateInfo | FileDateError;
82
+ }>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Get formatted diff for a specific file
3
+ * @param {Object} options - Options for getting formatted diff
4
+ * @param {string} options.filePath - Relative path from git root
5
+ * @param {string} options.gitRootDir - Absolute path to git root directory
6
+ * @param {string} options.baseBranch - Base branch to compare against
7
+ * @param {boolean} options.shrinkToLongestLine - Auto-calculate width based on longest line
8
+ * @param {number} options.leftMargin - Number of spaces to add to the left of each line
9
+ * @param {number} options.width - Width of the diff output (ignored if shrinkToLongestLine is true)
10
+ * @param {boolean} options.hideHeader - Remove the file path header from the diff
11
+ */
12
+ export function getFormattedDiff({ filePath, gitRootDir, baseBranch, shrinkToLongestLine, leftMargin, width, hideHeader }: {
13
+ filePath: string;
14
+ gitRootDir: string;
15
+ baseBranch: string;
16
+ shrinkToLongestLine: boolean;
17
+ leftMargin: number;
18
+ width: number;
19
+ hideHeader: boolean;
20
+ }): Promise<any>;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Gets the contents of a file at a specific git commit
3
+ * @param {string} filePath - Path to the file (relative to git root)
4
+ * @param {string} sha - Git commit SHA or ref (e.g., 'HEAD', 'main', 'abc123')
5
+ * @param {Object} [options] - Options
6
+ * @param {string} [options.cwd] - Working directory (defaults to process.cwd())
7
+ * @returns {Promise<string|undefined>} File contents at that commit, or undefined if not found
8
+ * @example
9
+ * const contents = await getFileAtCommit('src/index.js', 'HEAD~1')
10
+ * console.log('Previous version:', contents)
11
+ *
12
+ * // With cwd option
13
+ * const contents = await getFileAtCommit('package.json', 'HEAD', { cwd: '/path/to/repo' })
14
+ */
15
+ export function getFileAtCommit(filePath: string, sha: string, options?: {
16
+ cwd?: string;
17
+ }): Promise<string | undefined>;
@@ -1 +1,12 @@
1
- export function localGetFileAtSHA(path: any, _repo: any, sha: any): Promise<any>;
1
+ /**
2
+ * Gets file contents at a specific git commit (internal)
3
+ * @param {string} path - File path relative to git root
4
+ * @param {string|null} _repo - Unused, kept for interface compatibility
5
+ * @param {string} sha - Git commit SHA or ref
6
+ * @param {Object} [options] - Options
7
+ * @param {string} [options.cwd] - Working directory (defaults to process.cwd())
8
+ * @returns {Promise<string|undefined>} File contents or undefined if not found
9
+ */
10
+ export function localGetFileAtSHA(path: string, _repo: string | null, sha: string, options?: {
11
+ cwd?: string;
12
+ }): Promise<string | undefined>;
File without changes
File without changes
@@ -0,0 +1,15 @@
1
+ export = terminalSize;
2
+ declare function terminalSize(): TerminalSize;
3
+ declare namespace terminalSize {
4
+ export { TerminalSize };
5
+ }
6
+ type TerminalSize = {
7
+ /**
8
+ * - Number of columns in the terminal
9
+ */
10
+ columns: number;
11
+ /**
12
+ * - Number of rows in the terminal
13
+ */
14
+ rows: number;
15
+ };
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Validates and sanitizes a file path to prevent command injection
3
+ * @param {string} filePath - The file path to validate
4
+ * @returns {string} The validated file path
5
+ * @throws {Error} If the file path contains suspicious characters
6
+ */
7
+ export function validateFilePath(filePath: string): string;
package/types/index.d.ts CHANGED
@@ -1,7 +1,14 @@
1
+ import { gitDetails } from "./git/getDetails";
2
+ import { getGitRoot } from "./git/getGitRoot";
1
3
  import { getCommit } from "./git/commits/getCommit";
4
+ import { getAllCommits } from "./git/commits/getAllCommits";
2
5
  import { getFirstCommit } from "./git/commits/getFirstCommit";
3
6
  import { getLastCommit } from "./git/commits/getLastCommit";
4
- import { getAllCommits } from "./git/commits/getAllCommits";
5
- import { gitDetails } from "./git/getDetails";
6
7
  import { getGitFiles } from "./git/getGitFiles";
7
- export { getCommit, getFirstCommit, getLastCommit, getAllCommits, gitDetails, getGitFiles };
8
+ import { getFileAtCommit } from "./git/getFileAtCommit";
9
+ import { getFileModifiedTimeStamp } from "./git/dates/getFileDates";
10
+ import { getFileCreatedTimeStamp } from "./git/dates/getFileDates";
11
+ import { getFileDates } from "./git/dates/getFileDates";
12
+ import { getRemotes } from "./git/remotes/getRemotes";
13
+ import { getRemote } from "./git/remotes/getRemotes";
14
+ export { gitDetails, getGitRoot, getCommit, getAllCommits, getFirstCommit, getLastCommit, getGitFiles, getFileAtCommit, getFileModifiedTimeStamp, getFileCreatedTimeStamp, getFileDates, getRemotes, getRemote };
package/types/types.d.ts CHANGED
@@ -126,3 +126,21 @@ export type GitDetails = {
126
126
  */
127
127
  linesOfCode: () => Promise<number>;
128
128
  };
129
+ export type FileDateInfo = {
130
+ /**
131
+ * - Unix timestamp (seconds) of when the file was first committed
132
+ */
133
+ created: number;
134
+ /**
135
+ * - Unix timestamp (seconds) of when the file was last modified
136
+ */
137
+ modified: number;
138
+ /**
139
+ * - JavaScript Date object of creation time
140
+ */
141
+ createdDate: Date;
142
+ /**
143
+ * - JavaScript Date object of last modification
144
+ */
145
+ modifiedDate: Date;
146
+ };