@jayrdeaton/scripts 1.0.0
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 +85 -0
- package/bin/cli.mjs +24 -0
- package/package.json +37 -0
- package/src/commands/bump-ota.mjs +55 -0
- package/src/commands/code-count.mjs +87 -0
- package/src/commands/folder-sizes.mjs +67 -0
- package/src/commands/rename-season.mjs +45 -0
- package/src/commands/repo-status.mjs +94 -0
- package/src/commands/update-deps.mjs +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# @jayrdeaton/scripts
|
|
2
|
+
|
|
3
|
+
Personal dev CLI. Invoked as `jrd`.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install -g @jayrdeaton/scripts
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
### `jrd bump-ota`
|
|
14
|
+
|
|
15
|
+
Bumps `otaVersion` in `src/constants/release.ts` and auto-commits the change. Requires a clean working directory.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
jrd bump-ota [options]
|
|
19
|
+
|
|
20
|
+
Options:
|
|
21
|
+
-f, --file <file> Path to release file (default: src/constants/release.ts)
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
### `jrd code-count`
|
|
27
|
+
|
|
28
|
+
Count lines of code by file extension across one or more paths.
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
jrd code-count [paths...] [options]
|
|
32
|
+
|
|
33
|
+
Options:
|
|
34
|
+
-i, --ignore <types...> Ignore files or file types (e.g. .json)
|
|
35
|
+
-r, --recursive Scan folders recursively
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
### `jrd folder-sizes`
|
|
41
|
+
|
|
42
|
+
List all subdirectories sorted by size, largest first.
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
jrd folder-sizes [dir]
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
### `jrd rename-season`
|
|
51
|
+
|
|
52
|
+
Rename files in a directory to `SxEE` format for TV library pickup (e.g. `1x01.mkv`, `1x02.mkv`).
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
jrd rename-season <season> [dir]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### `jrd repo-status`
|
|
61
|
+
|
|
62
|
+
Scan a directory of git repos and report which ones have dirty files, untracked files, or unpushed commits.
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
jrd repo-status [dir]
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
### `jrd update-deps`
|
|
71
|
+
|
|
72
|
+
Update all npm dependencies to `@latest`. Automatically runs `npx expo install --fix` if the project uses Expo.
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
jrd update-deps [options]
|
|
76
|
+
|
|
77
|
+
Options:
|
|
78
|
+
-d, --dev Only update devDependencies
|
|
79
|
+
-p, --prod Only update dependencies
|
|
80
|
+
-l, --legacy Pass --legacy-peer-deps to npm install
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Requirements
|
|
84
|
+
|
|
85
|
+
Node >= 20
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readdirSync } from 'node:fs'
|
|
3
|
+
import { dirname, resolve } from 'node:path'
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
|
|
6
|
+
import { command, parse } from 'termkit'
|
|
7
|
+
|
|
8
|
+
const __dir = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
const commandsDir = resolve(__dir, '../src/commands')
|
|
10
|
+
|
|
11
|
+
// Must be created first so termkit registers this as the root command
|
|
12
|
+
const program = command('jrd').description('Personal dev scripts')
|
|
13
|
+
|
|
14
|
+
const files = readdirSync(commandsDir).filter((f) => f.endsWith('.mjs'))
|
|
15
|
+
const mods = await Promise.all(files.map((f) => import(`../src/commands/${f}`)))
|
|
16
|
+
|
|
17
|
+
program.commands(mods.map((m) => m.command))
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
await parse(process.argv)
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error(err.message)
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jayrdeaton/scripts",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Personal dev scripts",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"jrd": "./bin/cli.mjs"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"src"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"fix": "eslint --fix",
|
|
17
|
+
"lint": "eslint"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"cosmetic": "latest",
|
|
21
|
+
"termkit": "latest"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"eslint": "^10.4.1",
|
|
25
|
+
"eslint-config-prettier": "^10.1.8",
|
|
26
|
+
"eslint-plugin-package-json": "^1.3.0",
|
|
27
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
28
|
+
"eslint-plugin-simple-import-sort": "^13.0.0",
|
|
29
|
+
"prettier": "^3.8.3"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import cosmetic from 'cosmetic'
|
|
6
|
+
import { command as createCommand } from 'termkit'
|
|
7
|
+
|
|
8
|
+
export const command = createCommand('bump-ota')
|
|
9
|
+
.description('Bump otaVersion in src/constants/release.ts and commit')
|
|
10
|
+
.option('f', 'file', '[file]', 'Path to release file (default: src/constants/release.ts)')
|
|
11
|
+
.action(async (options) => {
|
|
12
|
+
const filePath = resolve(process.cwd(), options.file ?? 'src/constants/release.ts')
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const status = execSync('git status --porcelain').toString().trim()
|
|
16
|
+
if (status) {
|
|
17
|
+
console.error(cosmetic.red('Working directory is not clean. Commit or stash changes first.'))
|
|
18
|
+
process.exit(1)
|
|
19
|
+
}
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.error(cosmetic.red(`Failed to check git status: ${err.message}`))
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let source
|
|
26
|
+
try {
|
|
27
|
+
source = readFileSync(filePath, 'utf8')
|
|
28
|
+
} catch {
|
|
29
|
+
console.error(cosmetic.red(`Could not read file: ${filePath}`))
|
|
30
|
+
process.exit(1)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const pattern = /(otaVersion:\s*)(\d+)/
|
|
34
|
+
const match = source.match(pattern)
|
|
35
|
+
|
|
36
|
+
if (!match) {
|
|
37
|
+
console.error(cosmetic.red(`Could not find otaVersion in ${filePath}`))
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const current = parseInt(match[2], 10)
|
|
42
|
+
const next = current + 1
|
|
43
|
+
|
|
44
|
+
writeFileSync(filePath, source.replace(pattern, `$1${next}`))
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
execSync(`git add ${filePath}`)
|
|
48
|
+
execSync(`git commit -m "otaVersion ${current} -> ${next}"`)
|
|
49
|
+
} catch (err) {
|
|
50
|
+
console.error(cosmetic.red(`Auto-commit failed: ${err.message}`))
|
|
51
|
+
process.exit(1)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(cosmetic.bold.green(`otaVersion bumped: ${current} -> ${next}`))
|
|
55
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createReadStream, existsSync, lstatSync, readdirSync } from 'node:fs'
|
|
2
|
+
import { extname, join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import cosmetic from 'cosmetic'
|
|
5
|
+
import { command as createCommand } from 'termkit'
|
|
6
|
+
|
|
7
|
+
const WHITELIST = new Set([
|
|
8
|
+
'.cjs', '.css', '.csv', '.ejs', '.env', '.gitignore', '.haml', '.html', '.java',
|
|
9
|
+
'.js', '.json', '.mjs', '.paw', '.plist', '.py', '.rake', '.scss', '.sh', '.sql',
|
|
10
|
+
'.stl', '.swift', '.ts', '.tsx', '.txt', '.xib', '.xml', '.yaml', '.yml',
|
|
11
|
+
])
|
|
12
|
+
|
|
13
|
+
const SKIP = new Set([
|
|
14
|
+
'.DS_Store', '.git', 'Carthage', 'Dockerfile', 'LICENSE', 'Test',
|
|
15
|
+
'node_modules', 'package-lock.json',
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
function getFiles(base, { ignore = [], recursive = false } = {}) {
|
|
19
|
+
try {
|
|
20
|
+
base = resolve(base)
|
|
21
|
+
if (!lstatSync(base).isDirectory()) {
|
|
22
|
+
return existsSync(base) ? [base] : []
|
|
23
|
+
}
|
|
24
|
+
const paths = []
|
|
25
|
+
for (const item of readdirSync(base)) {
|
|
26
|
+
const ext = extname(item)
|
|
27
|
+
if (SKIP.has(item) || ignore.includes(item) || ignore.includes(`*${ext}`)) continue
|
|
28
|
+
const full = join(base, item)
|
|
29
|
+
const isDir = lstatSync(full).isDirectory()
|
|
30
|
+
if (recursive && isDir) {
|
|
31
|
+
paths.push(...getFiles(full, { ignore, recursive }))
|
|
32
|
+
} else if (!isDir && WHITELIST.has(ext)) {
|
|
33
|
+
paths.push(full)
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return paths
|
|
37
|
+
} catch (err) {
|
|
38
|
+
console.log(`error reading ${base}: ${err.message}`)
|
|
39
|
+
return []
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function countLines(path) {
|
|
44
|
+
return new Promise((res) => {
|
|
45
|
+
let count = 0
|
|
46
|
+
createReadStream(path)
|
|
47
|
+
.on('data', (chunk) => {
|
|
48
|
+
for (let i = 0; i < chunk.length; i++) if (chunk[i] === 10) count++
|
|
49
|
+
})
|
|
50
|
+
.on('error', () => res(0))
|
|
51
|
+
.on('end', () => res(count))
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function commaString(n) {
|
|
56
|
+
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const command = createCommand('code-count')
|
|
60
|
+
.description('Count lines of code by file type')
|
|
61
|
+
.variable('[paths...]')
|
|
62
|
+
.option('i', 'ignore', '[types...]', 'ignore files or file types')
|
|
63
|
+
.option('r', 'recursive', null, 'scan folders recursively')
|
|
64
|
+
.action(async (args) => {
|
|
65
|
+
let { ignore, paths, recursive } = args
|
|
66
|
+
if (!paths || paths.length === 0) paths = ['.']
|
|
67
|
+
if (!ignore) ignore = []
|
|
68
|
+
ignore = ignore.reduce((a, i) => (a.includes(`*${extname(i)}`) ? a : [...a, `*${extname(i)}`]), [])
|
|
69
|
+
|
|
70
|
+
const allPaths = paths.flatMap((p) => getFiles(p, { ignore, recursive }))
|
|
71
|
+
|
|
72
|
+
const totals = {}
|
|
73
|
+
for (let i = 0; i < allPaths.length; i++) {
|
|
74
|
+
const path = allPaths[i]
|
|
75
|
+
const ext = extname(path)
|
|
76
|
+
if (!ext) continue
|
|
77
|
+
process.stdout.write(`checking files ${i + 1} / ${allPaths.length}\r`)
|
|
78
|
+
totals[ext] = (totals[ext] ?? 0) + (await countLines(path))
|
|
79
|
+
}
|
|
80
|
+
if (process.stdout.clearLine) process.stdout.clearLine(0)
|
|
81
|
+
|
|
82
|
+
for (const key of Object.keys(totals).sort()) {
|
|
83
|
+
console.log(`${key}: ${cosmetic.cyan(commaString(totals[key]))}`)
|
|
84
|
+
}
|
|
85
|
+
const total = Object.values(totals).reduce((a, n) => a + n, 0)
|
|
86
|
+
console.log(`total ${cosmetic.cyan(commaString(total))} in ${cosmetic.cyan(String(allPaths.length))} files`)
|
|
87
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { readdirSync, statSync } from 'node:fs'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import cosmetic from 'cosmetic'
|
|
5
|
+
import { command as createCommand } from 'termkit'
|
|
6
|
+
|
|
7
|
+
function getDirSize(dirPath) {
|
|
8
|
+
let total = 0
|
|
9
|
+
try {
|
|
10
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
11
|
+
const full = join(dirPath, entry.name)
|
|
12
|
+
if (entry.isSymbolicLink()) continue
|
|
13
|
+
if (entry.isDirectory()) {
|
|
14
|
+
total += getDirSize(full)
|
|
15
|
+
} else {
|
|
16
|
+
try {
|
|
17
|
+
total += statSync(full).size
|
|
18
|
+
} catch {}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
} catch {}
|
|
22
|
+
return total
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function formatSize(bytes) {
|
|
26
|
+
if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`
|
|
27
|
+
if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`
|
|
28
|
+
if (bytes >= 1_024) return `${(bytes / 1_024).toFixed(1)} KB`
|
|
29
|
+
return `${bytes} B`
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const command = createCommand('folder-sizes')
|
|
33
|
+
.description('List folders sorted by size, largest first')
|
|
34
|
+
.variable('[dir]')
|
|
35
|
+
.action(async (args) => {
|
|
36
|
+
const root = resolve(args.dir ?? '.')
|
|
37
|
+
|
|
38
|
+
let entries
|
|
39
|
+
try {
|
|
40
|
+
entries = readdirSync(root, { withFileTypes: true })
|
|
41
|
+
} catch {
|
|
42
|
+
console.error(cosmetic.red(`Could not read directory: ${root}`))
|
|
43
|
+
process.exit(1)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const folders = entries
|
|
47
|
+
.filter((e) => e.isDirectory())
|
|
48
|
+
.map((e) => {
|
|
49
|
+
const size = getDirSize(join(root, e.name))
|
|
50
|
+
return { name: e.name, size }
|
|
51
|
+
})
|
|
52
|
+
.sort((a, b) => b.size - a.size)
|
|
53
|
+
|
|
54
|
+
if (folders.length === 0) {
|
|
55
|
+
console.log(cosmetic.faint('No folders found.'))
|
|
56
|
+
return
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const maxName = Math.max(...folders.map((f) => f.name.length))
|
|
60
|
+
const maxSize = Math.max(...folders.map((f) => formatSize(f.size).length))
|
|
61
|
+
|
|
62
|
+
for (const folder of folders) {
|
|
63
|
+
const name = folder.name.padEnd(maxName)
|
|
64
|
+
const size = formatSize(folder.size).padStart(maxSize)
|
|
65
|
+
console.log(` ${cosmetic.cyan(name)} ${cosmetic.bold(size)}`)
|
|
66
|
+
}
|
|
67
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readdirSync, renameSync, statSync } from 'node:fs'
|
|
2
|
+
import { extname, join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import cosmetic from 'cosmetic'
|
|
5
|
+
import { command as createCommand } from 'termkit'
|
|
6
|
+
|
|
7
|
+
export const command = createCommand('rename-season')
|
|
8
|
+
.description('Rename files in a directory to SxEE format for TV library pickup')
|
|
9
|
+
.variable('<season> [dir]')
|
|
10
|
+
.action(async (args) => {
|
|
11
|
+
const { season, dir = '.' } = args
|
|
12
|
+
|
|
13
|
+
let files
|
|
14
|
+
try {
|
|
15
|
+
files = readdirSync(dir)
|
|
16
|
+
} catch {
|
|
17
|
+
console.error(cosmetic.red(`Could not read directory: ${dir}`))
|
|
18
|
+
process.exit(1)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let counter = 1
|
|
22
|
+
let renamed = 0
|
|
23
|
+
|
|
24
|
+
for (const file of files) {
|
|
25
|
+
if (file.startsWith('.')) continue
|
|
26
|
+
|
|
27
|
+
const filePath = join(dir, file)
|
|
28
|
+
if (!statSync(filePath).isFile()) continue
|
|
29
|
+
|
|
30
|
+
const ext = extname(file)
|
|
31
|
+
const episode = counter.toString().padStart(2, '0')
|
|
32
|
+
const newName = `${season}x${episode}${ext}`
|
|
33
|
+
const newPath = join(dir, newName)
|
|
34
|
+
|
|
35
|
+
if (filePath !== newPath) {
|
|
36
|
+
console.log(`${cosmetic.faint(file)} -> ${cosmetic.cyan(newName)}`)
|
|
37
|
+
renameSync(filePath, newPath)
|
|
38
|
+
renamed++
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
counter++
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
console.log(cosmetic.bold.green(`\nDone. Renamed ${renamed} files.`))
|
|
45
|
+
})
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { readdirSync, statSync } from 'node:fs'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import cosmetic from 'cosmetic'
|
|
6
|
+
import { command as createCommand } from 'termkit'
|
|
7
|
+
|
|
8
|
+
function getGitStatus(dir) {
|
|
9
|
+
try {
|
|
10
|
+
const out = execSync('git status --porcelain', {
|
|
11
|
+
cwd: dir,
|
|
12
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
13
|
+
encoding: 'utf8',
|
|
14
|
+
})
|
|
15
|
+
const lines = out.split('\n').filter(Boolean)
|
|
16
|
+
const dirty = lines.filter((l) => !l.startsWith('??'))
|
|
17
|
+
const untracked = lines.filter((l) => l.startsWith('??'))
|
|
18
|
+
|
|
19
|
+
let unpushed = 0
|
|
20
|
+
try {
|
|
21
|
+
const ahead = execSync('git rev-list --count @{u}..HEAD', {
|
|
22
|
+
cwd: dir,
|
|
23
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
24
|
+
encoding: 'utf8',
|
|
25
|
+
}).trim()
|
|
26
|
+
unpushed = parseInt(ahead, 10) || 0
|
|
27
|
+
} catch {
|
|
28
|
+
// no upstream configured
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return { isRepo: true, dirty, untracked, unpushed }
|
|
32
|
+
} catch {
|
|
33
|
+
return { isRepo: false }
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const command = createCommand('repo-status')
|
|
38
|
+
.description('Report dirty and untracked files across repos in a directory')
|
|
39
|
+
.variable('[dir]')
|
|
40
|
+
.action(async (args) => {
|
|
41
|
+
const root = resolve(args.dir ?? '.')
|
|
42
|
+
|
|
43
|
+
let entries
|
|
44
|
+
try {
|
|
45
|
+
entries = readdirSync(root)
|
|
46
|
+
} catch {
|
|
47
|
+
console.error(cosmetic.red(`Could not read directory: ${root}`))
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const repos = entries
|
|
52
|
+
.filter((name) => {
|
|
53
|
+
try {
|
|
54
|
+
return statSync(join(root, name)).isDirectory()
|
|
55
|
+
} catch {
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
})
|
|
59
|
+
.map((name) => ({ name, ...getGitStatus(join(root, name)) }))
|
|
60
|
+
.filter((r) => r.isRepo)
|
|
61
|
+
|
|
62
|
+
const dirty = repos.filter((r) => r.dirty.length > 0)
|
|
63
|
+
const untracked = repos.filter((r) => r.untracked.length > 0)
|
|
64
|
+
const unpushed = repos.filter((r) => r.unpushed > 0)
|
|
65
|
+
const clean = repos.filter((r) => r.dirty.length === 0 && r.untracked.length === 0 && r.unpushed === 0)
|
|
66
|
+
|
|
67
|
+
if (dirty.length === 0 && untracked.length === 0 && unpushed.length === 0) {
|
|
68
|
+
console.log(cosmetic.green(`All ${clean.length} repos are clean.`))
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (dirty.length > 0) {
|
|
73
|
+
console.log(cosmetic.bold.red(`\nDirty (${dirty.length})`))
|
|
74
|
+
for (const repo of dirty) {
|
|
75
|
+
console.log(` ${cosmetic.red(repo.name)} ${cosmetic.faint(`${repo.dirty.length} change${repo.dirty.length !== 1 ? 's' : ''}`)}`)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (untracked.length > 0) {
|
|
80
|
+
console.log(cosmetic.bold.yellow(`\nUntracked (${untracked.length})`))
|
|
81
|
+
for (const repo of untracked) {
|
|
82
|
+
console.log(` ${cosmetic.yellow(repo.name)} ${cosmetic.faint(`${repo.untracked.length} file${repo.untracked.length !== 1 ? 's' : ''}`)}`)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (unpushed.length > 0) {
|
|
87
|
+
console.log(cosmetic.bold.cyan(`\nUnpushed (${unpushed.length})`))
|
|
88
|
+
for (const repo of unpushed) {
|
|
89
|
+
console.log(` ${cosmetic.cyan(repo.name)} ${cosmetic.faint(`${repo.unpushed} commit${repo.unpushed !== 1 ? 's' : ''} ahead`)}`)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
console.log(cosmetic.faint(`\n${clean.length} of ${repos.length} repos clean`))
|
|
94
|
+
})
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import cosmetic from 'cosmetic'
|
|
6
|
+
import { command as createCommand } from 'termkit'
|
|
7
|
+
|
|
8
|
+
function exec(cmd) {
|
|
9
|
+
console.log(cosmetic.faint(`$ ${cmd}`))
|
|
10
|
+
execSync(cmd, { stdio: 'inherit' })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function latestPackages(deps = {}) {
|
|
14
|
+
return Object.keys(deps).map((name) => `${name}@latest`)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const command = createCommand('update-deps')
|
|
18
|
+
.description('Update all npm deps to @latest, then run expo install --fix if applicable')
|
|
19
|
+
.option('d', 'dev', null, 'Only update devDependencies')
|
|
20
|
+
.option('p', 'prod', null, 'Only update dependencies')
|
|
21
|
+
.option('l', 'legacy', null, 'Pass --legacy-peer-deps to npm install')
|
|
22
|
+
.action(async (options) => {
|
|
23
|
+
const pkgPath = resolve(process.cwd(), 'package.json')
|
|
24
|
+
let pkg
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
28
|
+
} catch {
|
|
29
|
+
console.error(cosmetic.red('No package.json found in current directory.'))
|
|
30
|
+
process.exit(1)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const prodDeps = latestPackages(pkg.dependencies)
|
|
34
|
+
const devDeps = latestPackages(pkg.devDependencies)
|
|
35
|
+
const legacyFlag = options.legacy ? ' --legacy-peer-deps' : ''
|
|
36
|
+
|
|
37
|
+
if (!options.dev && prodDeps.length) {
|
|
38
|
+
console.log(cosmetic.bold.cyan('\nUpdating dependencies...'))
|
|
39
|
+
exec(`npm install${legacyFlag} ${prodDeps.join(' ')}`)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!options.prod && devDeps.length) {
|
|
43
|
+
console.log(cosmetic.bold.cyan('\nUpdating devDependencies...'))
|
|
44
|
+
exec(`npm install --save-dev${legacyFlag} ${devDeps.join(' ')}`)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const hasExpo = pkg.dependencies?.expo !== undefined || pkg.devDependencies?.expo !== undefined
|
|
48
|
+
|
|
49
|
+
if (hasExpo) {
|
|
50
|
+
console.log(cosmetic.bold.cyan('\nFixing Expo managed versions...'))
|
|
51
|
+
exec(`npx expo install --fix${options.legacy ? ' -- --legacy-peer-deps' : ''}`)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(cosmetic.bold.green('\nDone.'))
|
|
55
|
+
})
|