@jayrdeaton/scripts 1.0.0 → 1.0.2

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/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@jayrdeaton/scripts",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "private": false,
5
5
  "description": "Personal dev scripts",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/jayrdeaton/Scripts"
9
+ },
6
10
  "license": "MIT",
7
11
  "type": "module",
8
12
  "bin": {
@@ -13,12 +17,18 @@
13
17
  "src"
14
18
  ],
15
19
  "scripts": {
16
- "fix": "eslint --fix",
17
- "lint": "eslint"
20
+ "fix": "npm run lint:fix",
21
+ "lint": "eslint",
22
+ "lint:fix": "eslint --fix",
23
+ "release:major": "npm version major && git push --follow-tags",
24
+ "release:minor": "npm version minor && git push --follow-tags",
25
+ "release:patch": "npm version patch && git push --follow-tags",
26
+ "preversion": "npm ci && npm run lint"
18
27
  },
19
28
  "dependencies": {
20
29
  "cosmetic": "latest",
21
- "termkit": "latest"
30
+ "termkit": "latest",
31
+ "termpulse": "^1.1.3"
22
32
  },
23
33
  "devDependencies": {
24
34
  "eslint": "^10.4.1",
@@ -28,10 +38,10 @@
28
38
  "eslint-plugin-simple-import-sort": "^13.0.0",
29
39
  "prettier": "^3.8.3"
30
40
  },
31
- "publishConfig": {
32
- "access": "public"
33
- },
34
41
  "engines": {
35
42
  "node": ">=20"
43
+ },
44
+ "publishConfig": {
45
+ "access": "public"
36
46
  }
37
47
  }
@@ -2,23 +2,25 @@ 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 cosmetic from 'cosmetic'
6
5
  import { command as createCommand } from 'termkit'
6
+ import { Spinner } from 'termpulse'
7
7
 
8
8
  export const command = createCommand('bump-ota')
9
9
  .description('Bump otaVersion in src/constants/release.ts and commit')
10
10
  .option('f', 'file', '[file]', 'Path to release file (default: src/constants/release.ts)')
11
11
  .action(async (options) => {
12
12
  const filePath = resolve(process.cwd(), options.file ?? 'src/constants/release.ts')
13
+ const spinner = new Spinner({ text: 'Checking git status...' })
14
+ spinner.start()
13
15
 
14
16
  try {
15
17
  const status = execSync('git status --porcelain').toString().trim()
16
18
  if (status) {
17
- console.error(cosmetic.red('Working directory is not clean. Commit or stash changes first.'))
19
+ spinner.fail('Working directory is not clean. Commit or stash changes first.')
18
20
  process.exit(1)
19
21
  }
20
22
  } catch (err) {
21
- console.error(cosmetic.red(`Failed to check git status: ${err.message}`))
23
+ spinner.fail(`Failed to check git status: ${err.message}`)
22
24
  process.exit(1)
23
25
  }
24
26
 
@@ -26,7 +28,7 @@ export const command = createCommand('bump-ota')
26
28
  try {
27
29
  source = readFileSync(filePath, 'utf8')
28
30
  } catch {
29
- console.error(cosmetic.red(`Could not read file: ${filePath}`))
31
+ spinner.fail(`Could not read file: ${filePath}`)
30
32
  process.exit(1)
31
33
  }
32
34
 
@@ -34,7 +36,7 @@ export const command = createCommand('bump-ota')
34
36
  const match = source.match(pattern)
35
37
 
36
38
  if (!match) {
37
- console.error(cosmetic.red(`Could not find otaVersion in ${filePath}`))
39
+ spinner.fail(`Could not find otaVersion in ${filePath}`)
38
40
  process.exit(1)
39
41
  }
40
42
 
@@ -43,13 +45,14 @@ export const command = createCommand('bump-ota')
43
45
 
44
46
  writeFileSync(filePath, source.replace(pattern, `$1${next}`))
45
47
 
48
+ spinner.message('Committing...')
46
49
  try {
47
50
  execSync(`git add ${filePath}`)
48
51
  execSync(`git commit -m "otaVersion ${current} -> ${next}"`)
49
52
  } catch (err) {
50
- console.error(cosmetic.red(`Auto-commit failed: ${err.message}`))
53
+ spinner.fail(`Auto-commit failed: ${err.message}`)
51
54
  process.exit(1)
52
55
  }
53
56
 
54
- console.log(cosmetic.bold.green(`otaVersion bumped: ${current} -> ${next}`))
57
+ spinner.succeed(`otaVersion bumped: ${current} -> ${next}`)
55
58
  })
@@ -3,17 +3,11 @@ import { extname, join, resolve } from 'node:path'
3
3
 
4
4
  import cosmetic from 'cosmetic'
5
5
  import { command as createCommand } from 'termkit'
6
+ import { Spinner } from 'termpulse'
6
7
 
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
- ])
8
+ 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'])
12
9
 
13
- const SKIP = new Set([
14
- '.DS_Store', '.git', 'Carthage', 'Dockerfile', 'LICENSE', 'Test',
15
- 'node_modules', 'package-lock.json',
16
- ])
10
+ const SKIP = new Set(['.DS_Store', '.git', 'Carthage', 'Dockerfile', 'LICENSE', 'Test', 'node_modules', 'package-lock.json'])
17
11
 
18
12
  function getFiles(base, { ignore = [], recursive = false } = {}) {
19
13
  try {
@@ -69,19 +63,22 @@ export const command = createCommand('code-count')
69
63
 
70
64
  const allPaths = paths.flatMap((p) => getFiles(p, { ignore, recursive }))
71
65
 
66
+ const spinner = new Spinner({ text: 'Scanning...' })
67
+ spinner.start()
68
+
72
69
  const totals = {}
73
70
  for (let i = 0; i < allPaths.length; i++) {
74
71
  const path = allPaths[i]
75
72
  const ext = extname(path)
76
73
  if (!ext) continue
77
- process.stdout.write(`checking files ${i + 1} / ${allPaths.length}\r`)
74
+ spinner.message(`Checking files ${i + 1} / ${allPaths.length}`)
78
75
  totals[ext] = (totals[ext] ?? 0) + (await countLines(path))
79
76
  }
80
- if (process.stdout.clearLine) process.stdout.clearLine(0)
77
+
78
+ const total = Object.values(totals).reduce((a, n) => a + n, 0)
79
+ spinner.succeed(`${commaString(total)} lines across ${allPaths.length} files`)
81
80
 
82
81
  for (const key of Object.keys(totals).sort()) {
83
82
  console.log(`${key}: ${cosmetic.cyan(commaString(totals[key]))}`)
84
83
  }
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
84
  })
@@ -3,6 +3,7 @@ import { join, resolve } from 'node:path'
3
3
 
4
4
  import cosmetic from 'cosmetic'
5
5
  import { command as createCommand } from 'termkit'
6
+ import { Spinner } from 'termpulse'
6
7
 
7
8
  function getDirSize(dirPath) {
8
9
  let total = 0
@@ -43,19 +44,25 @@ export const command = createCommand('folder-sizes')
43
44
  process.exit(1)
44
45
  }
45
46
 
47
+ const spinner = new Spinner({ text: 'Scanning...' })
48
+ spinner.start()
49
+
46
50
  const folders = entries
47
51
  .filter((e) => e.isDirectory())
48
52
  .map((e) => {
53
+ spinner.message(e.name)
49
54
  const size = getDirSize(join(root, e.name))
50
55
  return { name: e.name, size }
51
56
  })
52
57
  .sort((a, b) => b.size - a.size)
53
58
 
54
59
  if (folders.length === 0) {
55
- console.log(cosmetic.faint('No folders found.'))
60
+ spinner.warn('No folders found.')
56
61
  return
57
62
  }
58
63
 
64
+ spinner.succeed(`${folders.length} folders`)
65
+
59
66
  const maxName = Math.max(...folders.map((f) => f.name.length))
60
67
  const maxSize = Math.max(...folders.map((f) => formatSize(f.size).length))
61
68
 
@@ -0,0 +1,97 @@
1
+ import { execSync } from 'node:child_process'
2
+
3
+ import cosmetic from 'cosmetic'
4
+ import { command as createCommand } from 'termkit'
5
+ import { Spinner } from 'termpulse'
6
+
7
+ async function fetchJson(url) {
8
+ const res = await fetch(url)
9
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${url}`)
10
+ return res.json()
11
+ }
12
+
13
+ async function getAllPackages(username) {
14
+ const packages = []
15
+ const size = 250
16
+ let from = 0
17
+
18
+ while (true) {
19
+ const data = await fetchJson(`https://registry.npmjs.org/-/v1/search?text=maintainer:${username}&size=${size}&from=${from}`)
20
+ packages.push(...data.objects.map((o) => o.package.name))
21
+ if (packages.length >= data.total || data.objects.length < size) break
22
+ from += size
23
+ }
24
+
25
+ return packages
26
+ }
27
+
28
+ async function getDownloads(pkg, period) {
29
+ try {
30
+ const data = await fetchJson(`https://api.npmjs.org/downloads/point/${period}/${pkg}`)
31
+ return data.downloads ?? 0
32
+ } catch {
33
+ return 0
34
+ }
35
+ }
36
+
37
+ export const command = createCommand('npm-downloads')
38
+ .description('List all your npm packages sorted by total downloads')
39
+ .option('u', 'user', '<name>', 'npm username (defaults to npm whoami)')
40
+ .option('p', 'period', '<period>', 'last-day | last-week | last-month | last-year (default: last-month)')
41
+ .option('m', 'mtd', null, 'Use month-to-date instead of rolling 30 days')
42
+ .action(async (options) => {
43
+ const spinner = new Spinner({ text: 'Resolving username...' })
44
+ spinner.start()
45
+
46
+ let username = options.user
47
+ if (!username) {
48
+ try {
49
+ username = execSync('npm whoami', {
50
+ encoding: 'utf8',
51
+ stdio: ['ignore', 'pipe', 'ignore']
52
+ }).trim()
53
+ } catch {
54
+ spinner.fail('Could not determine npm username. Use --user <name> or run `npm login`.')
55
+ process.exit(1)
56
+ }
57
+ }
58
+
59
+ let period = options.period ?? 'last-month'
60
+ if (options.mtd) {
61
+ const now = new Date()
62
+ const yyyy = now.getFullYear()
63
+ const mm = String(now.getMonth() + 1).padStart(2, '0')
64
+ const dd = String(now.getDate()).padStart(2, '0')
65
+ period = `${yyyy}-${mm}-01:${yyyy}-${mm}-${dd}`
66
+ }
67
+
68
+ spinner.message(`Fetching packages for ${username}...`)
69
+ const packages = await getAllPackages(username)
70
+
71
+ if (packages.length === 0) {
72
+ spinner.warn(`No packages found for ${username}.`)
73
+ return
74
+ }
75
+
76
+ spinner.message(`Fetching download counts (${period})...`)
77
+ const results = await Promise.all(packages.map(async (name) => ({ name, downloads: await getDownloads(name, period) })))
78
+
79
+ spinner.succeed(`${packages.length} packages · ${period}`)
80
+
81
+ const sorted = results.sort((a, b) => b.downloads - a.downloads)
82
+ const orgs = [...new Set(sorted.filter((p) => p.name.startsWith('@')).map((p) => p.name.split('/')[0]))]
83
+ const maxWidth = String(sorted[0]?.downloads ?? 0).length
84
+
85
+ console.log(`\n${cosmetic.bold(username)} — ${cosmetic.faint(period)}`)
86
+ if (orgs.length > 0) console.log(cosmetic.faint(`orgs: ${orgs.join(' ')}`))
87
+ console.log()
88
+
89
+ for (const { name, downloads } of sorted) {
90
+ const count = String(downloads).padStart(maxWidth)
91
+ const label = downloads === 0 ? cosmetic.faint(name) : name
92
+ console.log(` ${cosmetic.cyan(count)} ${label}`)
93
+ }
94
+
95
+ const total = sorted.reduce((sum, p) => sum + p.downloads, 0)
96
+ console.log(cosmetic.faint(`\n${total.toLocaleString()} total downloads across ${sorted.length} packages`))
97
+ })
@@ -3,6 +3,7 @@ import { extname, join } from 'node:path'
3
3
 
4
4
  import cosmetic from 'cosmetic'
5
5
  import { command as createCommand } from 'termkit'
6
+ import { log } from 'termpulse'
6
7
 
7
8
  export const command = createCommand('rename-season')
8
9
  .description('Rename files in a directory to SxEE format for TV library pickup')
@@ -14,7 +15,7 @@ export const command = createCommand('rename-season')
14
15
  try {
15
16
  files = readdirSync(dir)
16
17
  } catch {
17
- console.error(cosmetic.red(`Could not read directory: ${dir}`))
18
+ log.fail(`Could not read directory: ${dir}`)
18
19
  process.exit(1)
19
20
  }
20
21
 
@@ -41,5 +42,5 @@ export const command = createCommand('rename-season')
41
42
  counter++
42
43
  }
43
44
 
44
- console.log(cosmetic.bold.green(`\nDone. Renamed ${renamed} files.`))
45
+ log.succeed(`Renamed ${renamed} files.`)
45
46
  })
@@ -4,13 +4,14 @@ import { join, resolve } from 'node:path'
4
4
 
5
5
  import cosmetic from 'cosmetic'
6
6
  import { command as createCommand } from 'termkit'
7
+ import { Spinner } from 'termpulse'
7
8
 
8
9
  function getGitStatus(dir) {
9
10
  try {
10
11
  const out = execSync('git status --porcelain', {
11
12
  cwd: dir,
12
13
  stdio: ['ignore', 'pipe', 'ignore'],
13
- encoding: 'utf8',
14
+ encoding: 'utf8'
14
15
  })
15
16
  const lines = out.split('\n').filter(Boolean)
16
17
  const dirty = lines.filter((l) => !l.startsWith('??'))
@@ -21,7 +22,7 @@ function getGitStatus(dir) {
21
22
  const ahead = execSync('git rev-list --count @{u}..HEAD', {
22
23
  cwd: dir,
23
24
  stdio: ['ignore', 'pipe', 'ignore'],
24
- encoding: 'utf8',
25
+ encoding: 'utf8'
25
26
  }).trim()
26
27
  unpushed = parseInt(ahead, 10) || 0
27
28
  } catch {
@@ -48,6 +49,9 @@ export const command = createCommand('repo-status')
48
49
  process.exit(1)
49
50
  }
50
51
 
52
+ const spinner = new Spinner({ text: 'Checking repos...' })
53
+ spinner.start()
54
+
51
55
  const repos = entries
52
56
  .filter((name) => {
53
57
  try {
@@ -56,7 +60,10 @@ export const command = createCommand('repo-status')
56
60
  return false
57
61
  }
58
62
  })
59
- .map((name) => ({ name, ...getGitStatus(join(root, name)) }))
63
+ .map((name) => {
64
+ spinner.message(name)
65
+ return { name, ...getGitStatus(join(root, name)) }
66
+ })
60
67
  .filter((r) => r.isRepo)
61
68
 
62
69
  const dirty = repos.filter((r) => r.dirty.length > 0)
@@ -65,10 +72,12 @@ export const command = createCommand('repo-status')
65
72
  const clean = repos.filter((r) => r.dirty.length === 0 && r.untracked.length === 0 && r.unpushed === 0)
66
73
 
67
74
  if (dirty.length === 0 && untracked.length === 0 && unpushed.length === 0) {
68
- console.log(cosmetic.green(`All ${clean.length} repos are clean.`))
75
+ spinner.succeed(`All ${clean.length} repos are clean.`)
69
76
  return
70
77
  }
71
78
 
79
+ spinner.stop()
80
+
72
81
  if (dirty.length > 0) {
73
82
  console.log(cosmetic.bold.red(`\nDirty (${dirty.length})`))
74
83
  for (const repo of dirty) {
@@ -4,6 +4,7 @@ import { resolve } from 'node:path'
4
4
 
5
5
  import cosmetic from 'cosmetic'
6
6
  import { command as createCommand } from 'termkit'
7
+ import { log } from 'termpulse'
7
8
 
8
9
  function exec(cmd) {
9
10
  console.log(cosmetic.faint(`$ ${cmd}`))
@@ -26,7 +27,7 @@ export const command = createCommand('update-deps')
26
27
  try {
27
28
  pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
28
29
  } catch {
29
- console.error(cosmetic.red('No package.json found in current directory.'))
30
+ log.fail('No package.json found in current directory.')
30
31
  process.exit(1)
31
32
  }
32
33
 
@@ -35,21 +36,21 @@ export const command = createCommand('update-deps')
35
36
  const legacyFlag = options.legacy ? ' --legacy-peer-deps' : ''
36
37
 
37
38
  if (!options.dev && prodDeps.length) {
38
- console.log(cosmetic.bold.cyan('\nUpdating dependencies...'))
39
+ log.info('Updating dependencies...')
39
40
  exec(`npm install${legacyFlag} ${prodDeps.join(' ')}`)
40
41
  }
41
42
 
42
43
  if (!options.prod && devDeps.length) {
43
- console.log(cosmetic.bold.cyan('\nUpdating devDependencies...'))
44
+ log.info('Updating devDependencies...')
44
45
  exec(`npm install --save-dev${legacyFlag} ${devDeps.join(' ')}`)
45
46
  }
46
47
 
47
48
  const hasExpo = pkg.dependencies?.expo !== undefined || pkg.devDependencies?.expo !== undefined
48
49
 
49
50
  if (hasExpo) {
50
- console.log(cosmetic.bold.cyan('\nFixing Expo managed versions...'))
51
+ log.info('Fixing Expo managed versions...')
51
52
  exec(`npx expo install --fix${options.legacy ? ' -- --legacy-peer-deps' : ''}`)
52
53
  }
53
54
 
54
- console.log(cosmetic.bold.green('\nDone.'))
55
+ log.succeed('Done.')
55
56
  })