@jayrdeaton/scripts 1.0.2 → 1.1.1
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 +121 -0
- package/bin/cli.mjs +3 -3
- package/package.json +2 -4
- package/src/commands/base64.mjs +45 -0
- package/src/commands/binary.mjs +38 -0
- package/src/commands/bump-ota.mjs +2 -3
- package/src/commands/check-domains.mjs +102 -0
- package/src/commands/clean-builds.mjs +129 -0
- package/src/commands/clean-junk.mjs +123 -0
- package/src/commands/code-count.mjs +3 -5
- package/src/commands/find-dep.mjs +95 -0
- package/src/commands/focus.mjs +11 -0
- package/src/commands/folder-sizes.mjs +4 -6
- package/src/commands/new-expo-project.mjs +77 -0
- package/src/commands/npm-downloads.mjs +7 -9
- package/src/commands/npm-namer.mjs +75 -0
- package/src/commands/rename-season.mjs +3 -5
- package/src/commands/repo-status.mjs +10 -12
- package/src/commands/update-boilerplate.mjs +52 -0
- package/src/commands/update-deps.mjs +6 -5
package/README.md
CHANGED
|
@@ -10,6 +10,36 @@ npm install -g @jayrdeaton/scripts
|
|
|
10
10
|
|
|
11
11
|
## Commands
|
|
12
12
|
|
|
13
|
+
### `jrd base64`
|
|
14
|
+
|
|
15
|
+
Encode or decode base64 strings and files.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
jrd base64 encode <value> [options]
|
|
19
|
+
jrd base64 decode <value> [options]
|
|
20
|
+
|
|
21
|
+
Options:
|
|
22
|
+
-f, --file Treat value as a file path
|
|
23
|
+
-c, --copy Copy result to clipboard
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
### `jrd binary`
|
|
29
|
+
|
|
30
|
+
Encode a file to a binary string, or restore a binary string back to its original file.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
jrd binary encode <file> [options]
|
|
34
|
+
jrd binary decode <file> <destination>
|
|
35
|
+
|
|
36
|
+
Options (encode):
|
|
37
|
+
-c, --copy Copy result to clipboard
|
|
38
|
+
-o, --output <dest> Write result to a file
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
13
43
|
### `jrd bump-ota`
|
|
14
44
|
|
|
15
45
|
Bumps `otaVersion` in `src/constants/release.ts` and auto-commits the change. Requires a clean working directory.
|
|
@@ -23,6 +53,57 @@ Options:
|
|
|
23
53
|
|
|
24
54
|
---
|
|
25
55
|
|
|
56
|
+
### `jrd check-domains`
|
|
57
|
+
|
|
58
|
+
Check domain availability via RDAP using a wildcard pattern where `?` matches any letter. Supports `.com`, `.net`, `.org`, and `.io`.
|
|
59
|
+
|
|
60
|
+
```
|
|
61
|
+
jrd check-domains <pattern> [options]
|
|
62
|
+
|
|
63
|
+
Options:
|
|
64
|
+
-c, --concurrency <n> Concurrent requests (default: 5)
|
|
65
|
+
-o, --output <file> Write available domains to a file
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Example: `jrd check-domains ??fu.com`
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
### `jrd clean-builds`
|
|
73
|
+
|
|
74
|
+
Delete build artifacts (`build`, `dist`, `ios`, `android`) across one or more repos. Dry run by default.
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
jrd clean-builds [dir...] [options]
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
-m, --modules Also delete node_modules
|
|
81
|
+
-D, --delete Actually delete (default is dry run)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
### `jrd clean-junk`
|
|
87
|
+
|
|
88
|
+
Delete files and directories matching given criteria.
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
jrd clean-junk [dir] [options]
|
|
92
|
+
|
|
93
|
+
Options:
|
|
94
|
+
-i, --includes <str> Delete items whose name includes this string
|
|
95
|
+
-e, --excludes <str> Delete items whose name excludes this string
|
|
96
|
+
--extension <str> Delete files with this extension
|
|
97
|
+
-s, --size <mb> Delete items under this size in MB
|
|
98
|
+
-r, --recursive Scan directories recursively
|
|
99
|
+
-f, --force Skip confirmation
|
|
100
|
+
-v, --verbose Show matched items before deleting
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Example: `jrd clean-junk --includes .DS_Store -r`
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
26
107
|
### `jrd code-count`
|
|
27
108
|
|
|
28
109
|
Count lines of code by file extension across one or more paths.
|
|
@@ -37,6 +118,18 @@ Options:
|
|
|
37
118
|
|
|
38
119
|
---
|
|
39
120
|
|
|
121
|
+
### `jrd focus`
|
|
122
|
+
|
|
123
|
+
Bring an application to the front using AppleScript.
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
jrd focus [app]
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Default app is `Terminal`.
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
40
133
|
### `jrd folder-sizes`
|
|
41
134
|
|
|
42
135
|
List all subdirectories sorted by size, largest first.
|
|
@@ -47,6 +140,34 @@ jrd folder-sizes [dir]
|
|
|
47
140
|
|
|
48
141
|
---
|
|
49
142
|
|
|
143
|
+
### `jrd npm-downloads`
|
|
144
|
+
|
|
145
|
+
List all your npm packages sorted by total downloads.
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
jrd npm-downloads [options]
|
|
149
|
+
|
|
150
|
+
Options:
|
|
151
|
+
-u, --user <name> npm username (defaults to npm whoami)
|
|
152
|
+
-p, --period <period> last-day | last-week | last-month | last-year (default: last-month)
|
|
153
|
+
-m, --mtd Use month-to-date instead of rolling 30 days
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
### `jrd npm-namer`
|
|
159
|
+
|
|
160
|
+
Check npm package name availability. Automatically checks nospace, hyphenated, and underscored variants.
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
jrd npm-namer <name> [options]
|
|
164
|
+
|
|
165
|
+
Options:
|
|
166
|
+
-s, --synonyms Also check synonyms of the name
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
50
171
|
### `jrd rename-season`
|
|
51
172
|
|
|
52
173
|
Rename files in a directory to `SxEE` format for TV library pickup (e.g. `1x01.mkv`, `1x02.mkv`).
|
package/bin/cli.mjs
CHANGED
|
@@ -3,13 +3,13 @@ import { readdirSync } from 'node:fs'
|
|
|
3
3
|
import { dirname, resolve } from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
5
|
|
|
6
|
-
import {
|
|
6
|
+
import { Program } from 'termkit'
|
|
7
7
|
|
|
8
8
|
const __dir = dirname(fileURLToPath(import.meta.url))
|
|
9
9
|
const commandsDir = resolve(__dir, '../src/commands')
|
|
10
10
|
|
|
11
11
|
// Must be created first so termkit registers this as the root command
|
|
12
|
-
const program = command('jrd').description('Personal dev scripts')
|
|
12
|
+
const program = Program.command('jrd').description('Personal dev scripts')
|
|
13
13
|
|
|
14
14
|
const files = readdirSync(commandsDir).filter((f) => f.endsWith('.mjs'))
|
|
15
15
|
const mods = await Promise.all(files.map((f) => import(`../src/commands/${f}`)))
|
|
@@ -17,7 +17,7 @@ const mods = await Promise.all(files.map((f) => import(`../src/commands/${f}`)))
|
|
|
17
17
|
program.commands(mods.map((m) => m.command))
|
|
18
18
|
|
|
19
19
|
try {
|
|
20
|
-
await parse(process.argv)
|
|
20
|
+
await Program.parse(process.argv)
|
|
21
21
|
} catch (err) {
|
|
22
22
|
console.error(err.message)
|
|
23
23
|
process.exit(1)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jayrdeaton/scripts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Personal dev scripts",
|
|
6
6
|
"repository": {
|
|
@@ -26,9 +26,7 @@
|
|
|
26
26
|
"preversion": "npm ci && npm run lint"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"
|
|
30
|
-
"termkit": "latest",
|
|
31
|
-
"termpulse": "^1.1.3"
|
|
29
|
+
"termkit": "^2.2.0"
|
|
32
30
|
},
|
|
33
31
|
"devDependencies": {
|
|
34
32
|
"eslint": "^10.4.1",
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { execSync } from 'node:child_process'
|
|
3
|
+
|
|
4
|
+
import { Color, Program } from 'termkit'
|
|
5
|
+
|
|
6
|
+
function resolveInput(value, file) {
|
|
7
|
+
if (file) {
|
|
8
|
+
if (!existsSync(value)) throw new Error(`${value} not found`)
|
|
9
|
+
return readFileSync(value)
|
|
10
|
+
}
|
|
11
|
+
return value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function output(result, copy) {
|
|
15
|
+
if (copy) {
|
|
16
|
+
execSync('pbcopy', { input: result })
|
|
17
|
+
console.log(`${Color.green('Success:')} Copied to clipboard`)
|
|
18
|
+
} else {
|
|
19
|
+
console.log(result)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const command = Program.command('base64')
|
|
24
|
+
.description('Encode or decode base64')
|
|
25
|
+
.commands([
|
|
26
|
+
Program.command('encode')
|
|
27
|
+
.description('Encode a string or file to base64')
|
|
28
|
+
.variable('<value>')
|
|
29
|
+
.option('f', 'file', null, 'treat value as a file path')
|
|
30
|
+
.option('c', 'copy', null, 'copy result to clipboard')
|
|
31
|
+
.action(({ value, file, copy }) => {
|
|
32
|
+
const input = resolveInput(value, file)
|
|
33
|
+
const result = Buffer.from(input).toString('base64')
|
|
34
|
+
output(result, copy)
|
|
35
|
+
}),
|
|
36
|
+
|
|
37
|
+
Program.command('decode')
|
|
38
|
+
.description('Decode a base64 string')
|
|
39
|
+
.variable('<value>')
|
|
40
|
+
.option('c', 'copy', null, 'copy result to clipboard')
|
|
41
|
+
.action(({ value, copy }) => {
|
|
42
|
+
const result = Buffer.from(value, 'base64').toString('utf8')
|
|
43
|
+
output(result, copy)
|
|
44
|
+
}),
|
|
45
|
+
])
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { execSync } from 'node:child_process'
|
|
3
|
+
|
|
4
|
+
import { Color, Program } from 'termkit'
|
|
5
|
+
|
|
6
|
+
export const command = Program.command('binary')
|
|
7
|
+
.description('Encode or decode binary strings')
|
|
8
|
+
.commands([
|
|
9
|
+
Program.command('encode')
|
|
10
|
+
.description('Encode a file to a binary string')
|
|
11
|
+
.variable('<file>')
|
|
12
|
+
.option('c', 'copy', null, 'copy result to clipboard')
|
|
13
|
+
.option('o', 'output', '<dest>', 'write result to a file')
|
|
14
|
+
.action(({ file, copy, output }) => {
|
|
15
|
+
if (!existsSync(file)) throw new Error(`${file} not found`)
|
|
16
|
+
const result = JSON.stringify(readFileSync(file, 'binary'))
|
|
17
|
+
if (copy) {
|
|
18
|
+
execSync('pbcopy', { input: result })
|
|
19
|
+
console.log(`${Color.green('Success:')} Copied binary string to clipboard`)
|
|
20
|
+
} else if (output) {
|
|
21
|
+
writeFileSync(output, result)
|
|
22
|
+
console.log(`${Color.green('Success:')} Wrote binary string to ${output}`)
|
|
23
|
+
} else {
|
|
24
|
+
console.log(result)
|
|
25
|
+
}
|
|
26
|
+
}),
|
|
27
|
+
|
|
28
|
+
Program.command('decode')
|
|
29
|
+
.description('Restore a binary string file back to its original format')
|
|
30
|
+
.variable('<file>')
|
|
31
|
+
.variable('<destination>')
|
|
32
|
+
.action(({ file, destination }) => {
|
|
33
|
+
if (!existsSync(file)) throw new Error(`${file} not found`)
|
|
34
|
+
const buf = Buffer.from(JSON.parse(readFileSync(file, 'utf8')), 'binary')
|
|
35
|
+
writeFileSync(destination, buf)
|
|
36
|
+
console.log(`${Color.green('Success:')} Restored ${file} to ${destination}`)
|
|
37
|
+
}),
|
|
38
|
+
])
|
|
@@ -2,10 +2,9 @@ import { execSync } from 'node:child_process'
|
|
|
2
2
|
import { readFileSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import { resolve } from 'node:path'
|
|
4
4
|
|
|
5
|
-
import {
|
|
6
|
-
import { Spinner } from 'termpulse'
|
|
5
|
+
import { Program, Spinner } from 'termkit'
|
|
7
6
|
|
|
8
|
-
export const command =
|
|
7
|
+
export const command = Program.command('bump-ota')
|
|
9
8
|
.description('Bump otaVersion in src/constants/release.ts and commit')
|
|
10
9
|
.option('f', 'file', '[file]', 'Path to release file (default: src/constants/release.ts)')
|
|
11
10
|
.action(async (options) => {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
import { Color, Program, Spinner } from 'termkit'
|
|
4
|
+
|
|
5
|
+
const RDAP = {
|
|
6
|
+
com: 'https://rdap.verisign.com/com/v1',
|
|
7
|
+
net: 'https://rdap.verisign.com/net/v1',
|
|
8
|
+
org: 'https://rdap.publicinterestregistry.org/rdap/org/v1',
|
|
9
|
+
io: 'https://rdap.nic.io/v1'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ALPHA = 'abcdefghijklmnopqrstuvwxyz'.split('')
|
|
13
|
+
|
|
14
|
+
function expandPattern(pattern) {
|
|
15
|
+
const dot = pattern.lastIndexOf('.')
|
|
16
|
+
if (dot === -1) throw new Error(`Pattern must include a TLD, e.g. ??fu.com`)
|
|
17
|
+
const namePart = pattern.slice(0, dot).toLowerCase()
|
|
18
|
+
const tld = pattern.slice(dot + 1).toLowerCase()
|
|
19
|
+
if (!RDAP[tld]) throw new Error(`.${tld} is not supported — supported TLDs: ${Object.keys(RDAP).join(', ')}`)
|
|
20
|
+
|
|
21
|
+
function expand(s) {
|
|
22
|
+
const i = s.indexOf('?')
|
|
23
|
+
if (i === -1) return [s]
|
|
24
|
+
return ALPHA.flatMap((c) => expand(s.slice(0, i) + c + s.slice(i + 1)))
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return expand(namePart).map((n) => `${n}.${tld}`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function isAvailable(domain) {
|
|
31
|
+
const tld = domain.slice(domain.lastIndexOf('.') + 1)
|
|
32
|
+
const res = await fetch(`${RDAP[tld]}/domain/${domain}`, { signal: AbortSignal.timeout(10_000) })
|
|
33
|
+
if (res.status === 404) return true
|
|
34
|
+
if (res.ok) return false
|
|
35
|
+
throw new Error(`HTTP ${res.status}`)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function withConcurrency(items, limit, fn) {
|
|
39
|
+
let i = 0
|
|
40
|
+
async function worker() {
|
|
41
|
+
while (i < items.length) {
|
|
42
|
+
const item = items[i++]
|
|
43
|
+
await fn(item)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker))
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const command = Program.command('check-domains')
|
|
50
|
+
.description('Check domain availability via RDAP for a wildcard pattern (? = any letter)')
|
|
51
|
+
.variable('[pattern]')
|
|
52
|
+
.option('c', 'concurrency', '<n>', 'Concurrent requests (default: 5)')
|
|
53
|
+
.option('o', 'output', '<file>', 'Write available domains to a file')
|
|
54
|
+
.action(async (args) => {
|
|
55
|
+
const pattern = args.pattern
|
|
56
|
+
if (!pattern) {
|
|
57
|
+
console.error(Color.red('Usage: jrd check-domains <pattern> e.g. ??fu.com or ???.io'))
|
|
58
|
+
process.exit(1)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const concurrency = Math.max(1, parseInt(args.concurrency ?? '5', 10))
|
|
62
|
+
|
|
63
|
+
let domains
|
|
64
|
+
try {
|
|
65
|
+
domains = expandPattern(pattern)
|
|
66
|
+
} catch (err) {
|
|
67
|
+
console.error(Color.red(err.message))
|
|
68
|
+
process.exit(1)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const available = []
|
|
72
|
+
let checked = 0
|
|
73
|
+
let errors = 0
|
|
74
|
+
|
|
75
|
+
const spinner = new Spinner({ text: `0/${domains.length}` })
|
|
76
|
+
spinner.start()
|
|
77
|
+
|
|
78
|
+
await withConcurrency(domains, concurrency, async (domain) => {
|
|
79
|
+
try {
|
|
80
|
+
const ok = await isAvailable(domain)
|
|
81
|
+
checked++
|
|
82
|
+
spinner.message(`${checked}/${domains.length} ${Color.faint(domain)}`)
|
|
83
|
+
if (ok) {
|
|
84
|
+
available.push(domain)
|
|
85
|
+
spinner.stop()
|
|
86
|
+
console.log(` ${Color.green(domain)}`)
|
|
87
|
+
spinner.start()
|
|
88
|
+
spinner.message(`${checked}/${domains.length} ${Color.faint(domain)}`)
|
|
89
|
+
}
|
|
90
|
+
} catch {
|
|
91
|
+
errors++
|
|
92
|
+
checked++
|
|
93
|
+
}
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
if (args.output && available.length > 0) {
|
|
97
|
+
await writeFile(args.output, available.join('\n') + '\n')
|
|
98
|
+
spinner.succeed(`${available.length} available · saved to ${args.output}` + (errors > 0 ? Color.faint(` (${errors} errors)`) : ''))
|
|
99
|
+
} else {
|
|
100
|
+
spinner.succeed(`${available.length} available of ${domains.length}` + (errors > 0 ? Color.faint(` (${errors} errors)`) : ''))
|
|
101
|
+
}
|
|
102
|
+
})
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { existsSync, readdirSync, rmSync, statSync } from 'node:fs'
|
|
2
|
+
import { join, resolve } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { Color, Program, Spinner } from 'termkit'
|
|
5
|
+
|
|
6
|
+
const BUILD_TARGETS = ['build', 'dist', 'ios', 'android']
|
|
7
|
+
|
|
8
|
+
function getDirSize(dirPath) {
|
|
9
|
+
let total = 0
|
|
10
|
+
try {
|
|
11
|
+
for (const entry of readdirSync(dirPath, { withFileTypes: true })) {
|
|
12
|
+
const full = join(dirPath, entry.name)
|
|
13
|
+
if (entry.isSymbolicLink()) continue
|
|
14
|
+
if (entry.isDirectory()) {
|
|
15
|
+
total += getDirSize(full)
|
|
16
|
+
} else {
|
|
17
|
+
try {
|
|
18
|
+
total += statSync(full).size
|
|
19
|
+
} catch {}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
} catch {}
|
|
23
|
+
return total
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function formatSize(bytes) {
|
|
27
|
+
if (bytes >= 1_073_741_824) return `${(bytes / 1_073_741_824).toFixed(1)} GB`
|
|
28
|
+
if (bytes >= 1_048_576) return `${(bytes / 1_048_576).toFixed(1)} MB`
|
|
29
|
+
if (bytes >= 1_024) return `${(bytes / 1_024).toFixed(1)} KB`
|
|
30
|
+
return `${bytes} B`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function findTargets(projectDir, targets) {
|
|
34
|
+
return targets
|
|
35
|
+
.map((t) => join(projectDir, t))
|
|
36
|
+
.filter((p) => {
|
|
37
|
+
try {
|
|
38
|
+
return statSync(p).isDirectory()
|
|
39
|
+
} catch {
|
|
40
|
+
return false
|
|
41
|
+
}
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveProjects(dirs) {
|
|
46
|
+
const projects = []
|
|
47
|
+
for (const dir of dirs) {
|
|
48
|
+
const root = resolve(dir)
|
|
49
|
+
if (existsSync(join(root, 'package.json'))) {
|
|
50
|
+
projects.push({ root, path: root })
|
|
51
|
+
} else {
|
|
52
|
+
let entries
|
|
53
|
+
try {
|
|
54
|
+
entries = readdirSync(root, { withFileTypes: true })
|
|
55
|
+
} catch {
|
|
56
|
+
console.error(Color.red(`Could not read directory: ${root}`))
|
|
57
|
+
process.exit(1)
|
|
58
|
+
}
|
|
59
|
+
for (const e of entries) {
|
|
60
|
+
if (e.isDirectory()) projects.push({ root, path: join(root, e.name) })
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return projects
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const command = Program.command('clean-builds')
|
|
68
|
+
.description('Delete build artifacts across repos — dry run by default')
|
|
69
|
+
.variable('[dir...]')
|
|
70
|
+
.option('m', 'modules', null, 'Also delete node_modules')
|
|
71
|
+
.option('D', 'delete', null, 'Actually delete (default is dry run)')
|
|
72
|
+
.action(async (options) => {
|
|
73
|
+
const dirs = options.dir?.length ? options.dir : ['.']
|
|
74
|
+
const targets = [...BUILD_TARGETS]
|
|
75
|
+
if (options.modules) targets.push('node_modules')
|
|
76
|
+
|
|
77
|
+
const projects = resolveProjects(dirs)
|
|
78
|
+
|
|
79
|
+
const spinner = new Spinner({ text: 'Scanning...' })
|
|
80
|
+
spinner.start()
|
|
81
|
+
|
|
82
|
+
const toDelete = []
|
|
83
|
+
for (const { root, path: projectDir } of projects) {
|
|
84
|
+
spinner.message(projectDir.split('/').at(-1))
|
|
85
|
+
for (const targetPath of findTargets(projectDir, targets)) {
|
|
86
|
+
const size = getDirSize(targetPath)
|
|
87
|
+
toDelete.push({ root, path: targetPath, size })
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
spinner.stop()
|
|
92
|
+
|
|
93
|
+
if (toDelete.length === 0) {
|
|
94
|
+
console.log(Color.green('Nothing to clean.'))
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const totalSize = toDelete.reduce((sum, f) => sum + f.size, 0)
|
|
99
|
+
|
|
100
|
+
if (!options.delete) {
|
|
101
|
+
console.log(Color.bold.yellow('\nDry run — pass --delete to remove:\n'))
|
|
102
|
+
for (const { root, path, size } of toDelete) {
|
|
103
|
+
const rel = path.replace(root + '/', '')
|
|
104
|
+
console.log(` ${Color.red(rel)} ${Color.faint(formatSize(size))}`)
|
|
105
|
+
}
|
|
106
|
+
console.log(Color.faint(`\n${toDelete.length} folder${toDelete.length !== 1 ? 's' : ''} ${formatSize(totalSize)} total`))
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const deleteSpinner = new Spinner({ text: 'Deleting...' })
|
|
111
|
+
deleteSpinner.start()
|
|
112
|
+
|
|
113
|
+
let deleted = 0
|
|
114
|
+
let freed = 0
|
|
115
|
+
for (const { path, size } of toDelete) {
|
|
116
|
+
deleteSpinner.message(path.split('/').slice(-2).join('/'))
|
|
117
|
+
try {
|
|
118
|
+
rmSync(path, { recursive: true, force: true })
|
|
119
|
+
deleted++
|
|
120
|
+
freed += size
|
|
121
|
+
} catch (err) {
|
|
122
|
+
deleteSpinner.stop()
|
|
123
|
+
console.error(Color.red(` Failed: ${path} — ${err.message}`))
|
|
124
|
+
deleteSpinner.start()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
deleteSpinner.succeed(`Deleted ${deleted} folder${deleted !== 1 ? 's' : ''}, ${formatSize(freed)} freed`)
|
|
129
|
+
})
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { extname, basename } from 'node:path'
|
|
2
|
+
import { readdirSync, rmSync, statSync } from 'node:fs'
|
|
3
|
+
import { createInterface } from 'node:readline'
|
|
4
|
+
import { join, resolve } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { Color, Program, Spinner } from 'termkit'
|
|
7
|
+
|
|
8
|
+
function prompt(question) {
|
|
9
|
+
return new Promise((res) => {
|
|
10
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout })
|
|
11
|
+
rl.question(question, (answer) => {
|
|
12
|
+
rl.close()
|
|
13
|
+
res(answer)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getItems(dir, recursive) {
|
|
19
|
+
const items = []
|
|
20
|
+
try {
|
|
21
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
22
|
+
const full = join(dir, entry.name)
|
|
23
|
+
items.push(full)
|
|
24
|
+
if (recursive && entry.isDirectory()) items.push(...getItems(full, recursive))
|
|
25
|
+
}
|
|
26
|
+
} catch {}
|
|
27
|
+
return items
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getItemSize(itemPath) {
|
|
31
|
+
let total = 0
|
|
32
|
+
try {
|
|
33
|
+
const stat = statSync(itemPath)
|
|
34
|
+
if (!stat.isDirectory()) return stat.size
|
|
35
|
+
for (const entry of readdirSync(itemPath, { withFileTypes: true })) {
|
|
36
|
+
const full = join(itemPath, entry.name)
|
|
37
|
+
if (entry.isSymbolicLink()) continue
|
|
38
|
+
total += entry.isDirectory() ? getItemSize(full) : statSync(full).size
|
|
39
|
+
}
|
|
40
|
+
} catch {}
|
|
41
|
+
return total
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export const command = Program.command('clean-junk')
|
|
45
|
+
.description('Delete files and directories matching given criteria')
|
|
46
|
+
.variable('[dir]')
|
|
47
|
+
.option('i', 'includes', '<str>', 'Delete items whose name includes this string')
|
|
48
|
+
.option('e', 'excludes', '<str>', 'Delete items whose name excludes this string')
|
|
49
|
+
.option(null, 'extension', '<str>', 'Delete files with this extension')
|
|
50
|
+
.option('s', 'size', '<mb>', 'Delete items under this size in MB')
|
|
51
|
+
.option('r', 'recursive', null, 'Scan directories recursively')
|
|
52
|
+
.option('f', 'force', null, 'Skip confirmation')
|
|
53
|
+
.option('v', 'verbose', null, 'Show matched items before deleting')
|
|
54
|
+
.action(async (options) => {
|
|
55
|
+
let { extension, force, verbose } = options
|
|
56
|
+
const dir = resolve(options.dir ?? '.')
|
|
57
|
+
const { includes, excludes, recursive, size } = options
|
|
58
|
+
|
|
59
|
+
if (extension && !extension.startsWith('.')) extension = `.${extension}`
|
|
60
|
+
|
|
61
|
+
if (!includes && !excludes && !extension && !size) {
|
|
62
|
+
console.error(Color.red('Requires at least one filter: --includes, --excludes, --extension, or --size'))
|
|
63
|
+
process.exit(1)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const spinner = new Spinner({ text: 'Scanning...' })
|
|
67
|
+
spinner.start()
|
|
68
|
+
const items = getItems(dir, recursive)
|
|
69
|
+
spinner.stop()
|
|
70
|
+
|
|
71
|
+
const sizeBytes = size ? size * 1_048_576 : null
|
|
72
|
+
const targets = []
|
|
73
|
+
|
|
74
|
+
const filterSpinner = new Spinner({ text: 'Filtering...' })
|
|
75
|
+
filterSpinner.start()
|
|
76
|
+
|
|
77
|
+
for (const item of items) {
|
|
78
|
+
const name = basename(item)
|
|
79
|
+
let match = true
|
|
80
|
+
if (includes && !name.includes(includes)) match = false
|
|
81
|
+
if (excludes && name.includes(excludes)) match = false
|
|
82
|
+
if (extension && extname(item) !== extension) match = false
|
|
83
|
+
if (sizeBytes !== null && getItemSize(item) > sizeBytes) match = false
|
|
84
|
+
if (match) targets.push(item)
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
filterSpinner.stop()
|
|
88
|
+
|
|
89
|
+
if (targets.length === 0) {
|
|
90
|
+
console.log(Color.green('No junk items found.'))
|
|
91
|
+
return
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (verbose) {
|
|
95
|
+
for (const item of targets) console.log(` ${item}`)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!force) {
|
|
99
|
+
const answer = await prompt(`Delete ${targets.length} item${targets.length !== 1 ? 's' : ''}? (Y/n) `)
|
|
100
|
+
if (answer.toLowerCase() !== 'y') {
|
|
101
|
+
console.log('Aborted.')
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const deleteSpinner = new Spinner({ text: 'Deleting...' })
|
|
107
|
+
deleteSpinner.start()
|
|
108
|
+
|
|
109
|
+
let deleted = 0
|
|
110
|
+
for (const item of targets) {
|
|
111
|
+
deleteSpinner.message(basename(item))
|
|
112
|
+
try {
|
|
113
|
+
rmSync(item, { recursive: true, force: true })
|
|
114
|
+
deleted++
|
|
115
|
+
} catch (err) {
|
|
116
|
+
deleteSpinner.stop()
|
|
117
|
+
console.error(Color.red(` Failed: ${item} — ${err.message}`))
|
|
118
|
+
deleteSpinner.start()
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
deleteSpinner.succeed(`Deleted ${deleted} item${deleted !== 1 ? 's' : ''}`)
|
|
123
|
+
})
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { createReadStream, existsSync, lstatSync, readdirSync } from 'node:fs'
|
|
2
2
|
import { extname, join, resolve } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import { command as createCommand } from 'termkit'
|
|
6
|
-
import { Spinner } from 'termpulse'
|
|
4
|
+
import { Color, Program, Spinner } from 'termkit'
|
|
7
5
|
|
|
8
6
|
const WHITELIST = new Set(['.cjs', '.css', '.csv', '.ejs', '.env', '.gitignore', '.haml', '.html', '.java', '.js', '.json', '.mjs', '.paw', '.plist', '.py', '.rake', '.scss', '.sh', '.sql', '.stl', '.swift', '.ts', '.tsx', '.txt', '.xib', '.xml', '.yaml', '.yml'])
|
|
9
7
|
|
|
@@ -50,7 +48,7 @@ function commaString(n) {
|
|
|
50
48
|
return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
export const command =
|
|
51
|
+
export const command = Program.command('code-count')
|
|
54
52
|
.description('Count lines of code by file type')
|
|
55
53
|
.variable('[paths...]')
|
|
56
54
|
.option('i', 'ignore', '[types...]', 'ignore files or file types')
|
|
@@ -79,6 +77,6 @@ export const command = createCommand('code-count')
|
|
|
79
77
|
spinner.succeed(`${commaString(total)} lines across ${allPaths.length} files`)
|
|
80
78
|
|
|
81
79
|
for (const key of Object.keys(totals).sort()) {
|
|
82
|
-
console.log(`${key}: ${
|
|
80
|
+
console.log(`${key}: ${Color.cyan(commaString(totals[key]))}`)
|
|
83
81
|
}
|
|
84
82
|
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
2
|
+
import { homedir } from 'node:os'
|
|
3
|
+
import { join, resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { Color, Program, Spinner } from 'termkit'
|
|
6
|
+
|
|
7
|
+
const DEP_FIELDS = ['dependencies', 'devDependencies', 'peerDependencies']
|
|
8
|
+
|
|
9
|
+
function findMatches(pkgPath, targets) {
|
|
10
|
+
let pkg
|
|
11
|
+
try {
|
|
12
|
+
pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
13
|
+
} catch {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const found = []
|
|
18
|
+
for (const field of DEP_FIELDS) {
|
|
19
|
+
if (!pkg[field]) continue
|
|
20
|
+
for (const target of targets) {
|
|
21
|
+
if (pkg[field][target] !== undefined) {
|
|
22
|
+
found.push({ name: target, version: pkg[field][target], field })
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return found.length ? { projectName: pkg.name, found } : null
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export const command = Program.command('find-dep', '[deps...]')
|
|
31
|
+
.description('Find projects in a directory that use any of the given dependencies')
|
|
32
|
+
.option('d', 'dir', '[dir]', 'Root directory to scan (default: ~/Developer)')
|
|
33
|
+
.action(async (options) => {
|
|
34
|
+
const targets = options.deps ?? []
|
|
35
|
+
|
|
36
|
+
if (!targets.length) {
|
|
37
|
+
console.error(Color.red('Provide at least one dependency name.'))
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const root = resolve(options.dir ?? join(homedir(), 'Developer'))
|
|
42
|
+
|
|
43
|
+
let entries
|
|
44
|
+
try {
|
|
45
|
+
entries = readdirSync(root)
|
|
46
|
+
} catch {
|
|
47
|
+
console.error(Color.red(`Could not read directory: ${root}`))
|
|
48
|
+
process.exit(1)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const spinner = new Spinner({ text: 'Scanning projects...' })
|
|
52
|
+
spinner.start()
|
|
53
|
+
|
|
54
|
+
const results = []
|
|
55
|
+
|
|
56
|
+
for (const name of entries) {
|
|
57
|
+
const dir = join(root, name)
|
|
58
|
+
try {
|
|
59
|
+
if (!statSync(dir).isDirectory()) continue
|
|
60
|
+
} catch {
|
|
61
|
+
continue
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
spinner.message(name)
|
|
65
|
+
const pkgPath = join(dir, 'package.json')
|
|
66
|
+
const match = findMatches(pkgPath, targets)
|
|
67
|
+
if (match) results.push({ dir: name, ...match })
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!results.length) {
|
|
71
|
+
spinner.succeed(`No projects found using: ${targets.join(', ')}`)
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
spinner.stop()
|
|
76
|
+
|
|
77
|
+
console.log(Color.bold(`\nFound ${results.length} project${results.length !== 1 ? 's' : ''}:\n`))
|
|
78
|
+
|
|
79
|
+
for (const result of results) {
|
|
80
|
+
const label = result.projectName && result.projectName !== result.dir
|
|
81
|
+
? `${Color.bold(result.dir)} ${Color.faint(`(${result.projectName})`)}`
|
|
82
|
+
: Color.bold(result.dir)
|
|
83
|
+
|
|
84
|
+
console.log(` ${label}`)
|
|
85
|
+
|
|
86
|
+
for (const dep of result.found) {
|
|
87
|
+
const fieldLabel = dep.field === 'dependencies' ? 'dep'
|
|
88
|
+
: dep.field === 'devDependencies' ? 'dev'
|
|
89
|
+
: 'peer'
|
|
90
|
+
console.log(` ${Color.cyan(dep.name)} ${Color.faint(`${dep.version} [${fieldLabel}]`)}`)
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
console.log()
|
|
95
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
|
|
3
|
+
import { Program } from 'termkit'
|
|
4
|
+
|
|
5
|
+
export const command = Program.command('focus')
|
|
6
|
+
.description('Bring an application to the front')
|
|
7
|
+
.variable('[app]')
|
|
8
|
+
.action((args) => {
|
|
9
|
+
const app = args.app ?? 'Terminal'
|
|
10
|
+
execSync(`osascript -e 'tell application "${app}" to activate'`)
|
|
11
|
+
})
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { readdirSync, statSync } from 'node:fs'
|
|
2
2
|
import { join, resolve } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import { command as createCommand } from 'termkit'
|
|
6
|
-
import { Spinner } from 'termpulse'
|
|
4
|
+
import { Color, Program, Spinner } from 'termkit'
|
|
7
5
|
|
|
8
6
|
function getDirSize(dirPath) {
|
|
9
7
|
let total = 0
|
|
@@ -30,7 +28,7 @@ function formatSize(bytes) {
|
|
|
30
28
|
return `${bytes} B`
|
|
31
29
|
}
|
|
32
30
|
|
|
33
|
-
export const command =
|
|
31
|
+
export const command = Program.command('folder-sizes')
|
|
34
32
|
.description('List folders sorted by size, largest first')
|
|
35
33
|
.variable('[dir]')
|
|
36
34
|
.action(async (args) => {
|
|
@@ -40,7 +38,7 @@ export const command = createCommand('folder-sizes')
|
|
|
40
38
|
try {
|
|
41
39
|
entries = readdirSync(root, { withFileTypes: true })
|
|
42
40
|
} catch {
|
|
43
|
-
console.error(
|
|
41
|
+
console.error(Color.red(`Could not read directory: ${root}`))
|
|
44
42
|
process.exit(1)
|
|
45
43
|
}
|
|
46
44
|
|
|
@@ -69,6 +67,6 @@ export const command = createCommand('folder-sizes')
|
|
|
69
67
|
for (const folder of folders) {
|
|
70
68
|
const name = folder.name.padEnd(maxName)
|
|
71
69
|
const size = formatSize(folder.size).padStart(maxSize)
|
|
72
|
-
console.log(` ${
|
|
70
|
+
console.log(` ${Color.cyan(name)} ${Color.bold(size)}`)
|
|
73
71
|
}
|
|
74
72
|
})
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { cpSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { Color, Program, log } from 'termkit'
|
|
7
|
+
|
|
8
|
+
const BOILERPLATE_REPO = 'git@github.com:jayrdeaton/Expo-Boilerplate.git'
|
|
9
|
+
const BOILERPLATE_DIR = join(homedir(), 'Developer', 'Expo-Boilerplate')
|
|
10
|
+
const DEV_DIR = join(homedir(), 'Developer')
|
|
11
|
+
|
|
12
|
+
function exec(cmd, opts = {}) {
|
|
13
|
+
console.log(Color.faint(`$ ${cmd}`))
|
|
14
|
+
execSync(cmd, { stdio: 'inherit', ...opts })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function parseWords(name) {
|
|
18
|
+
return name
|
|
19
|
+
.replace(/([a-z])([A-Z])/g, '$1 $2')
|
|
20
|
+
.split(/[\s\-_]+/)
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const command = Program.command('new-expo-project')
|
|
25
|
+
.description('Bootstrap a new Expo project from the boilerplate')
|
|
26
|
+
.option('n', 'name', '<name>', 'Project name')
|
|
27
|
+
.action(async (options) => {
|
|
28
|
+
const words = parseWords(options.name)
|
|
29
|
+
const displayName = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')
|
|
30
|
+
const slug = words.map((w) => w.toLowerCase()).join('-')
|
|
31
|
+
const pascal = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join('')
|
|
32
|
+
const lower = words.map((w) => w.toLowerCase()).join('')
|
|
33
|
+
const dirName = words.map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join('-')
|
|
34
|
+
const targetDir = join(DEV_DIR, dirName)
|
|
35
|
+
|
|
36
|
+
if (existsSync(targetDir)) {
|
|
37
|
+
log.fail(`Directory already exists: ${targetDir}`)
|
|
38
|
+
process.exit(1)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (existsSync(BOILERPLATE_DIR)) {
|
|
42
|
+
log.info('Pulling latest boilerplate...')
|
|
43
|
+
exec('git pull', { cwd: BOILERPLATE_DIR })
|
|
44
|
+
} else {
|
|
45
|
+
log.info('Cloning boilerplate...')
|
|
46
|
+
exec(`git clone ${BOILERPLATE_REPO} "${BOILERPLATE_DIR}"`)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
log.info(`Creating ${displayName}...`)
|
|
50
|
+
cpSync(BOILERPLATE_DIR, targetDir, {
|
|
51
|
+
recursive: true,
|
|
52
|
+
filter: (src) => !src.includes('/node_modules/'),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
rmSync(join(targetDir, '.git'), { recursive: true, force: true })
|
|
56
|
+
|
|
57
|
+
const pkgPath = join(targetDir, 'package.json')
|
|
58
|
+
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
|
|
59
|
+
pkg.name = slug
|
|
60
|
+
writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n')
|
|
61
|
+
|
|
62
|
+
const appPath = join(targetDir, 'app.json')
|
|
63
|
+
const app = JSON.parse(readFileSync(appPath, 'utf8'))
|
|
64
|
+
app.expo.name = displayName
|
|
65
|
+
app.expo.slug = slug
|
|
66
|
+
app.expo.scheme = lower
|
|
67
|
+
app.expo.description = `${displayName} app`
|
|
68
|
+
app.expo.ios.bundleIdentifier = `com.infinitetoken.${pascal}`
|
|
69
|
+
app.expo.android.package = `com.infinitetoken.${lower}`
|
|
70
|
+
writeFileSync(appPath, JSON.stringify(app, null, 2) + '\n')
|
|
71
|
+
|
|
72
|
+
exec('git init', { cwd: targetDir })
|
|
73
|
+
exec('git add -A', { cwd: targetDir })
|
|
74
|
+
exec('git commit -m "Initial commit"', { cwd: targetDir })
|
|
75
|
+
|
|
76
|
+
log.succeed(`${displayName} created at ${targetDir}`)
|
|
77
|
+
})
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process'
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import { command as createCommand } from 'termkit'
|
|
5
|
-
import { Spinner } from 'termpulse'
|
|
3
|
+
import { Color, Program, Spinner } from 'termkit'
|
|
6
4
|
|
|
7
5
|
async function fetchJson(url) {
|
|
8
6
|
const res = await fetch(url)
|
|
@@ -34,7 +32,7 @@ async function getDownloads(pkg, period) {
|
|
|
34
32
|
}
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
export const command =
|
|
35
|
+
export const command = Program.command('npm-downloads')
|
|
38
36
|
.description('List all your npm packages sorted by total downloads')
|
|
39
37
|
.option('u', 'user', '<name>', 'npm username (defaults to npm whoami)')
|
|
40
38
|
.option('p', 'period', '<period>', 'last-day | last-week | last-month | last-year (default: last-month)')
|
|
@@ -82,16 +80,16 @@ export const command = createCommand('npm-downloads')
|
|
|
82
80
|
const orgs = [...new Set(sorted.filter((p) => p.name.startsWith('@')).map((p) => p.name.split('/')[0]))]
|
|
83
81
|
const maxWidth = String(sorted[0]?.downloads ?? 0).length
|
|
84
82
|
|
|
85
|
-
console.log(`\n${
|
|
86
|
-
if (orgs.length > 0) console.log(
|
|
83
|
+
console.log(`\n${Color.bold(username)} — ${Color.faint(period)}`)
|
|
84
|
+
if (orgs.length > 0) console.log(Color.faint(`orgs: ${orgs.join(' ')}`))
|
|
87
85
|
console.log()
|
|
88
86
|
|
|
89
87
|
for (const { name, downloads } of sorted) {
|
|
90
88
|
const count = String(downloads).padStart(maxWidth)
|
|
91
|
-
const label = downloads === 0 ?
|
|
92
|
-
console.log(` ${
|
|
89
|
+
const label = downloads === 0 ? Color.faint(name) : name
|
|
90
|
+
console.log(` ${Color.cyan(count)} ${label}`)
|
|
93
91
|
}
|
|
94
92
|
|
|
95
93
|
const total = sorted.reduce((sum, p) => sum + p.downloads, 0)
|
|
96
|
-
console.log(
|
|
94
|
+
console.log(Color.faint(`\n${total.toLocaleString()} total downloads across ${sorted.length} packages`))
|
|
97
95
|
})
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Color, Program, Spinner } from 'termkit'
|
|
2
|
+
|
|
3
|
+
async function isAvailable(name) {
|
|
4
|
+
try {
|
|
5
|
+
const encoded = name.startsWith('@') ? name.replace('/', '%2F') : name
|
|
6
|
+
const res = await fetch(`https://registry.npmjs.org/${encoded}`, { method: 'HEAD' })
|
|
7
|
+
return res.status === 404
|
|
8
|
+
} catch {
|
|
9
|
+
return null
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getVariants(name) {
|
|
14
|
+
const normalized = name.replace(/[-_]/g, ' ').trim()
|
|
15
|
+
if (!normalized.includes(' ')) return [normalized]
|
|
16
|
+
|
|
17
|
+
const nospace = normalized.replace(/ /g, '')
|
|
18
|
+
const hyphenated = normalized.replace(/ /g, '-')
|
|
19
|
+
const underscored = normalized.replace(/ /g, '_')
|
|
20
|
+
return [...new Set([nospace, hyphenated, underscored])]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function getSynonyms(word) {
|
|
24
|
+
try {
|
|
25
|
+
const base = word.replace(/[-_]/g, ' ').trim().split(' ')[0]
|
|
26
|
+
const res = await fetch(`https://api.datamuse.com/words?ml=${encodeURIComponent(base)}&max=20`)
|
|
27
|
+
if (!res.ok) return []
|
|
28
|
+
const data = await res.json()
|
|
29
|
+
return data.map((w) => w.word).filter((w) => !w.includes(' '))
|
|
30
|
+
} catch {
|
|
31
|
+
return []
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export const command = Program.command('npm-namer')
|
|
36
|
+
.description('Check npm package name availability, including variations')
|
|
37
|
+
.variable('<name>')
|
|
38
|
+
.option('s', 'synonyms', null, 'Also check synonyms of the name')
|
|
39
|
+
.action(async (args) => {
|
|
40
|
+
const { name, synonyms } = args
|
|
41
|
+
const spinner = new Spinner({ text: 'Checking availability...' })
|
|
42
|
+
spinner.start()
|
|
43
|
+
|
|
44
|
+
const namesToCheck = new Set(getVariants(name))
|
|
45
|
+
|
|
46
|
+
if (synonyms) {
|
|
47
|
+
spinner.message('Fetching synonyms...')
|
|
48
|
+
const syns = await getSynonyms(name)
|
|
49
|
+
for (const syn of syns) {
|
|
50
|
+
for (const variant of getVariants(syn)) {
|
|
51
|
+
namesToCheck.add(variant)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
spinner.message(`Checking ${namesToCheck.size} names...`)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const results = await Promise.all([...namesToCheck].map(async (n) => ({ name: n, available: await isAvailable(n) })))
|
|
58
|
+
|
|
59
|
+
spinner.stop()
|
|
60
|
+
|
|
61
|
+
const available = results.filter((r) => r.available === true)
|
|
62
|
+
const taken = results.filter((r) => r.available === false)
|
|
63
|
+
const errored = results.filter((r) => r.available === null)
|
|
64
|
+
|
|
65
|
+
console.log()
|
|
66
|
+
for (const { name: n, available: avail } of results) {
|
|
67
|
+
if (avail === true) console.log(` ${Color.green('✓')} ${n}`)
|
|
68
|
+
else if (avail === false) console.log(` ${Color.red('✗')} ${Color.faint(n)}`)
|
|
69
|
+
else console.log(` ${Color.yellow('?')} ${Color.faint(n)}`)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const parts = [`${available.length} available`, `${taken.length} taken`]
|
|
73
|
+
if (errored.length > 0) parts.push(`${errored.length} error`)
|
|
74
|
+
console.log(`\n${Color.faint(parts.join(' · '))}`)
|
|
75
|
+
})
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { readdirSync, renameSync, statSync } from 'node:fs'
|
|
2
2
|
import { extname, join } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import
|
|
5
|
-
import { command as createCommand } from 'termkit'
|
|
6
|
-
import { log } from 'termpulse'
|
|
4
|
+
import { Color, Program, log } from 'termkit'
|
|
7
5
|
|
|
8
|
-
export const command =
|
|
6
|
+
export const command = Program.command('rename-season')
|
|
9
7
|
.description('Rename files in a directory to SxEE format for TV library pickup')
|
|
10
8
|
.variable('<season> [dir]')
|
|
11
9
|
.action(async (args) => {
|
|
@@ -34,7 +32,7 @@ export const command = createCommand('rename-season')
|
|
|
34
32
|
const newPath = join(dir, newName)
|
|
35
33
|
|
|
36
34
|
if (filePath !== newPath) {
|
|
37
|
-
console.log(`${
|
|
35
|
+
console.log(`${Color.faint(file)} -> ${Color.cyan(newName)}`)
|
|
38
36
|
renameSync(filePath, newPath)
|
|
39
37
|
renamed++
|
|
40
38
|
}
|
|
@@ -2,9 +2,7 @@ import { execSync } from 'node:child_process'
|
|
|
2
2
|
import { readdirSync, statSync } from 'node:fs'
|
|
3
3
|
import { join, resolve } from 'node:path'
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import { command as createCommand } from 'termkit'
|
|
7
|
-
import { Spinner } from 'termpulse'
|
|
5
|
+
import { Color, Program, Spinner } from 'termkit'
|
|
8
6
|
|
|
9
7
|
function getGitStatus(dir) {
|
|
10
8
|
try {
|
|
@@ -35,7 +33,7 @@ function getGitStatus(dir) {
|
|
|
35
33
|
}
|
|
36
34
|
}
|
|
37
35
|
|
|
38
|
-
export const command =
|
|
36
|
+
export const command = Program.command('repo-status')
|
|
39
37
|
.description('Report dirty and untracked files across repos in a directory')
|
|
40
38
|
.variable('[dir]')
|
|
41
39
|
.action(async (args) => {
|
|
@@ -45,7 +43,7 @@ export const command = createCommand('repo-status')
|
|
|
45
43
|
try {
|
|
46
44
|
entries = readdirSync(root)
|
|
47
45
|
} catch {
|
|
48
|
-
console.error(
|
|
46
|
+
console.error(Color.red(`Could not read directory: ${root}`))
|
|
49
47
|
process.exit(1)
|
|
50
48
|
}
|
|
51
49
|
|
|
@@ -79,25 +77,25 @@ export const command = createCommand('repo-status')
|
|
|
79
77
|
spinner.stop()
|
|
80
78
|
|
|
81
79
|
if (dirty.length > 0) {
|
|
82
|
-
console.log(
|
|
80
|
+
console.log(Color.bold.red(`\nDirty (${dirty.length})`))
|
|
83
81
|
for (const repo of dirty) {
|
|
84
|
-
console.log(` ${
|
|
82
|
+
console.log(` ${Color.red(repo.name)} ${Color.faint(`${repo.dirty.length} change${repo.dirty.length !== 1 ? 's' : ''}`)}`)
|
|
85
83
|
}
|
|
86
84
|
}
|
|
87
85
|
|
|
88
86
|
if (untracked.length > 0) {
|
|
89
|
-
console.log(
|
|
87
|
+
console.log(Color.bold.yellow(`\nUntracked (${untracked.length})`))
|
|
90
88
|
for (const repo of untracked) {
|
|
91
|
-
console.log(` ${
|
|
89
|
+
console.log(` ${Color.yellow(repo.name)} ${Color.faint(`${repo.untracked.length} file${repo.untracked.length !== 1 ? 's' : ''}`)}`)
|
|
92
90
|
}
|
|
93
91
|
}
|
|
94
92
|
|
|
95
93
|
if (unpushed.length > 0) {
|
|
96
|
-
console.log(
|
|
94
|
+
console.log(Color.bold.cyan(`\nUnpushed (${unpushed.length})`))
|
|
97
95
|
for (const repo of unpushed) {
|
|
98
|
-
console.log(` ${
|
|
96
|
+
console.log(` ${Color.cyan(repo.name)} ${Color.faint(`${repo.unpushed} commit${repo.unpushed !== 1 ? 's' : ''} ahead`)}`)
|
|
99
97
|
}
|
|
100
98
|
}
|
|
101
99
|
|
|
102
|
-
console.log(
|
|
100
|
+
console.log(Color.faint(`\n${clean.length} of ${repos.length} repos clean`))
|
|
103
101
|
})
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process'
|
|
2
|
+
import { existsSync } from 'node:fs'
|
|
3
|
+
import { homedir } from 'node:os'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
|
|
6
|
+
import { Color, Program, log } from 'termkit'
|
|
7
|
+
|
|
8
|
+
const BOILERPLATE_REPO = 'git@github.com:jayrdeaton/Expo-Boilerplate.git'
|
|
9
|
+
const BOILERPLATE_DIR = join(homedir(), 'Developer', 'Expo-Boilerplate')
|
|
10
|
+
|
|
11
|
+
function exec(cmd, opts = {}) {
|
|
12
|
+
console.log(Color.faint(`$ ${cmd}`))
|
|
13
|
+
execSync(cmd, { stdio: 'inherit', ...opts })
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const command = Program.command('update-boilerplate')
|
|
17
|
+
.description('Update Expo boilerplate — clones if absent, updates deps, commits, and pushes')
|
|
18
|
+
.action(async () => {
|
|
19
|
+
if (existsSync(BOILERPLATE_DIR)) {
|
|
20
|
+
log.info('Boilerplate found, pulling latest...')
|
|
21
|
+
exec('git pull', { cwd: BOILERPLATE_DIR })
|
|
22
|
+
} else {
|
|
23
|
+
log.info('Cloning boilerplate...')
|
|
24
|
+
exec(`git clone ${BOILERPLATE_REPO} "${BOILERPLATE_DIR}"`)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
log.info('Updating dependencies...')
|
|
28
|
+
exec('jrd update-deps', { cwd: BOILERPLATE_DIR })
|
|
29
|
+
|
|
30
|
+
const status = execSync('git status --porcelain', { cwd: BOILERPLATE_DIR }).toString().trim()
|
|
31
|
+
|
|
32
|
+
if (!status) {
|
|
33
|
+
log.succeed('Nothing to commit — boilerplate is already up to date.')
|
|
34
|
+
return
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
log.info('Running lint fix...')
|
|
38
|
+
exec('npm run fix', { cwd: BOILERPLATE_DIR })
|
|
39
|
+
|
|
40
|
+
log.info('Running type check...')
|
|
41
|
+
exec('npm run check', { cwd: BOILERPLATE_DIR })
|
|
42
|
+
|
|
43
|
+
log.info('Running tests...')
|
|
44
|
+
exec('npm test', { cwd: BOILERPLATE_DIR })
|
|
45
|
+
|
|
46
|
+
log.info('Committing and pushing...')
|
|
47
|
+
exec('git add -A', { cwd: BOILERPLATE_DIR })
|
|
48
|
+
exec('git commit -m "Update dependencies"', { cwd: BOILERPLATE_DIR })
|
|
49
|
+
exec('git push', { cwd: BOILERPLATE_DIR })
|
|
50
|
+
|
|
51
|
+
log.succeed('Boilerplate updated.')
|
|
52
|
+
})
|
|
@@ -2,12 +2,10 @@ import { execSync } from 'node:child_process'
|
|
|
2
2
|
import { readFileSync } from 'node:fs'
|
|
3
3
|
import { resolve } from 'node:path'
|
|
4
4
|
|
|
5
|
-
import
|
|
6
|
-
import { command as createCommand } from 'termkit'
|
|
7
|
-
import { log } from 'termpulse'
|
|
5
|
+
import { Color, Program, log } from 'termkit'
|
|
8
6
|
|
|
9
7
|
function exec(cmd) {
|
|
10
|
-
console.log(
|
|
8
|
+
console.log(Color.faint(`$ ${cmd}`))
|
|
11
9
|
execSync(cmd, { stdio: 'inherit' })
|
|
12
10
|
}
|
|
13
11
|
|
|
@@ -15,7 +13,7 @@ function latestPackages(deps = {}) {
|
|
|
15
13
|
return Object.keys(deps).map((name) => `${name}@latest`)
|
|
16
14
|
}
|
|
17
15
|
|
|
18
|
-
export const command =
|
|
16
|
+
export const command = Program.command('update-deps')
|
|
19
17
|
.description('Update all npm deps to @latest, then run expo install --fix if applicable')
|
|
20
18
|
.option('d', 'dev', null, 'Only update devDependencies')
|
|
21
19
|
.option('p', 'prod', null, 'Only update dependencies')
|
|
@@ -52,5 +50,8 @@ export const command = createCommand('update-deps')
|
|
|
52
50
|
exec(`npx expo install --fix${options.legacy ? ' -- --legacy-peer-deps' : ''}`)
|
|
53
51
|
}
|
|
54
52
|
|
|
53
|
+
log.info('Auditing and fixing vulnerabilities...')
|
|
54
|
+
exec('npm audit --fix')
|
|
55
|
+
|
|
55
56
|
log.succeed('Done.')
|
|
56
57
|
})
|