@jayrdeaton/scripts 1.1.1 → 1.1.3

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
@@ -69,6 +69,22 @@ Example: `jrd check-domains ??fu.com`
69
69
 
70
70
  ---
71
71
 
72
+ ### `jrd check-scripts`
73
+
74
+ Compare `package.json` scripts across projects for consistency. Highlights scripts whose values differ from the most common value (or a reference project).
75
+
76
+ ```
77
+ jrd check-scripts [scripts...] [options]
78
+
79
+ Options:
80
+ -d, --dir <dir> Root directory to scan (default: ~/Developer)
81
+ -r, --ref <ref> Reference project name to compare against
82
+ -a, --all Show all scripts, including matching ones
83
+ -f, --flat Show one line per project instead of grouping by value
84
+ ```
85
+
86
+ ---
87
+
72
88
  ### `jrd clean-builds`
73
89
 
74
90
  Delete build artifacts (`build`, `dist`, `ios`, `android`) across one or more repos. Dry run by default.
@@ -118,6 +134,36 @@ Options:
118
134
 
119
135
  ---
120
136
 
137
+ ### `jrd find-dep`
138
+
139
+ Find projects in a directory that use any of the given dependencies (searches `dependencies`, `devDependencies`, and `peerDependencies`).
140
+
141
+ ```
142
+ jrd find-dep <deps...> [options]
143
+
144
+ Options:
145
+ -d, --dir <dir> Root directory to scan (default: ~/Developer)
146
+ ```
147
+
148
+ Example: `jrd find-dep react-native expo`
149
+
150
+ ---
151
+
152
+ ### `jrd find-script`
153
+
154
+ Find projects in a directory whose `package.json` scripts contain an exact command value.
155
+
156
+ ```
157
+ jrd find-script <command> [options]
158
+
159
+ Options:
160
+ -d, --dir <dir> Root directory to scan (default: ~/Developer)
161
+ ```
162
+
163
+ Example: `jrd find-script "eslint . --fix"`
164
+
165
+ ---
166
+
121
167
  ### `jrd focus`
122
168
 
123
169
  Bring an application to the front using AppleScript.
@@ -130,6 +176,21 @@ Default app is `Terminal`.
130
176
 
131
177
  ---
132
178
 
179
+ ### `jrd including`
180
+
181
+ Find projects in a directory that contain a given file.
182
+
183
+ ```
184
+ jrd including <file> [options]
185
+
186
+ Options:
187
+ -d, --dir <dir> Root directory to scan (default: ~/Developer)
188
+ ```
189
+
190
+ Example: `jrd including PLAN.md`
191
+
192
+ ---
193
+
133
194
  ### `jrd folder-sizes`
134
195
 
135
196
  List all subdirectories sorted by size, largest first.
@@ -140,6 +201,36 @@ jrd folder-sizes [dir]
140
201
 
141
202
  ---
142
203
 
204
+ ### `jrd missing`
205
+
206
+ Find projects in a directory that are missing a given file.
207
+
208
+ ```
209
+ jrd missing <file> [options]
210
+
211
+ Options:
212
+ -d, --dir <dir> Root directory to scan (default: ~/Developer)
213
+ ```
214
+
215
+ Example: `jrd missing README.md`
216
+
217
+ ---
218
+
219
+ ### `jrd new-expo-project`
220
+
221
+ Bootstrap a new Expo project from the boilerplate repo. Clones or updates the boilerplate, copies it to `~/Developer/<Name>`, rewrites `package.json` and `app.json` with the derived name/slug/bundle identifiers, and creates an initial git commit.
222
+
223
+ ```
224
+ jrd new-expo-project [options]
225
+
226
+ Options:
227
+ -n, --name <name> Project name (required)
228
+ ```
229
+
230
+ Example: `jrd new-expo-project --name MyApp`
231
+
232
+ ---
233
+
143
234
  ### `jrd npm-downloads`
144
235
 
145
236
  List all your npm packages sorted by total downloads.
@@ -178,6 +269,19 @@ jrd rename-season <season> [dir]
178
269
 
179
270
  ---
180
271
 
272
+ ### `jrd repo-rulesets`
273
+
274
+ Find public GitHub repos that have no ruleset attached. Requires `gh auth login`.
275
+
276
+ ```
277
+ jrd repo-rulesets [options]
278
+
279
+ Options:
280
+ -u, --user <user> GitHub username (defaults to authenticated user)
281
+ ```
282
+
283
+ ---
284
+
181
285
  ### `jrd repo-status`
182
286
 
183
287
  Scan a directory of git repos and report which ones have dirty files, untracked files, or unpushed commits.
@@ -188,6 +292,33 @@ jrd repo-status [dir]
188
292
 
189
293
  ---
190
294
 
295
+ ### `jrd sync-peers`
296
+
297
+ Sync `@rific` package `peerDependency` floors and `devDependency` versions to match Expo-Starter. Dry run by default.
298
+
299
+ ```
300
+ jrd sync-peers [options]
301
+
302
+ Options:
303
+ -s, --starter <path> Path to Expo-Starter project (default: ~/Developer/Expo-Starter)
304
+ -r, --root <path> Root directory containing @rific packages (default: ~/Developer)
305
+ -d, --dry Preview changes without writing
306
+ -i, --install Run npm install in each changed repo
307
+ -t, --test Run npm test in each changed repo (implies --install)
308
+ ```
309
+
310
+ ---
311
+
312
+ ### `jrd update-boilerplate`
313
+
314
+ Update the Expo boilerplate repo — clones it if absent, runs `jrd update-deps`, lint-fixes, type-checks, tests, then commits and pushes the result.
315
+
316
+ ```
317
+ jrd update-boilerplate
318
+ ```
319
+
320
+ ---
321
+
191
322
  ### `jrd update-deps`
192
323
 
193
324
  Update all npm dependencies to `@latest`. Automatically runs `npx expo install --fix` if the project uses Expo.
@@ -201,6 +332,19 @@ Options:
201
332
  -l, --legacy Pass --legacy-peer-deps to npm install
202
333
  ```
203
334
 
335
+ ### `jrd yalc-check`
336
+
337
+ Find projects in a directory that have yalc dependencies (version entries starting with `file:.yalc/` or a `yalc.lock` present).
338
+
339
+ ```
340
+ jrd yalc-check [options]
341
+
342
+ Options:
343
+ -d, --dir <dir> Root directory to scan (default: ~/Developer)
344
+ ```
345
+
346
+ ---
347
+
204
348
  ## Requirements
205
349
 
206
350
  Node >= 20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jayrdeaton/scripts",
3
- "version": "1.1.1",
3
+ "version": "1.1.3",
4
4
  "private": false,
5
5
  "description": "Personal dev scripts",
6
6
  "repository": {
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync } from 'node:fs'
2
1
  import { execSync } from 'node:child_process'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
3
 
4
4
  import { Color, Program } from 'termkit'
5
5
 
@@ -41,5 +41,5 @@ export const command = Program.command('base64')
41
41
  .action(({ value, copy }) => {
42
42
  const result = Buffer.from(value, 'base64').toString('utf8')
43
43
  output(result, copy)
44
- }),
44
+ })
45
45
  ])
@@ -1,5 +1,5 @@
1
- import { existsSync, readFileSync, writeFileSync } from 'node:fs'
2
1
  import { execSync } from 'node:child_process'
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs'
3
3
 
4
4
  import { Color, Program } from 'termkit'
5
5
 
@@ -34,5 +34,5 @@ export const command = Program.command('binary')
34
34
  const buf = Buffer.from(JSON.parse(readFileSync(file, 'utf8')), 'binary')
35
35
  writeFileSync(destination, buf)
36
36
  console.log(`${Color.green('Success:')} Restored ${file} to ${destination}`)
37
- }),
37
+ })
38
38
  ])
@@ -0,0 +1,145 @@
1
+ import { readdirSync, readFileSync, 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
+ function loadProject(pkgPath) {
8
+ try {
9
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
10
+ return { name: pkg.name, scripts: pkg.scripts ?? {} }
11
+ } catch {
12
+ return null
13
+ }
14
+ }
15
+
16
+ function mostCommon(values) {
17
+ const freq = {}
18
+ for (const v of values) freq[v] = (freq[v] ?? 0) + 1
19
+ return Object.entries(freq).sort((a, b) => b[1] - a[1])[0][0]
20
+ }
21
+
22
+ export const command = Program.command('check-scripts', '[scripts...]')
23
+ .description('Compare package.json scripts across projects for consistency')
24
+ .option('d', 'dir', '[dir]', 'Root directory to scan (default: ~/Developer)')
25
+ .option('r', 'ref', '[ref]', 'Reference project name to compare against')
26
+ .option('a', 'all', null, 'Show all scripts, including matching ones')
27
+ .option('f', 'flat', null, 'Show one line per project instead of grouping by value')
28
+ .action(async (options) => {
29
+ const root = resolve(options.dir ?? join(homedir(), 'Developer'))
30
+ const filterScripts = options.scripts ?? []
31
+
32
+ let entries
33
+ try {
34
+ entries = readdirSync(root)
35
+ } catch {
36
+ console.error(Color.red(`Could not read directory: ${root}`))
37
+ process.exit(1)
38
+ }
39
+
40
+ const spinner = new Spinner({ text: 'Scanning projects...' })
41
+ spinner.start()
42
+
43
+ const projects = []
44
+
45
+ for (const entry of entries) {
46
+ const dir = join(root, entry)
47
+ try {
48
+ if (!statSync(dir).isDirectory()) continue
49
+ } catch {
50
+ continue
51
+ }
52
+ spinner.message(entry)
53
+ const result = loadProject(join(dir, 'package.json'))
54
+ if (result) projects.push({ dir: entry, ...result })
55
+ }
56
+
57
+ spinner.stop()
58
+
59
+ if (!projects.length) {
60
+ console.log(Color.yellow('No projects with package.json found.'))
61
+ return
62
+ }
63
+
64
+ let refProject = null
65
+ if (options.ref) {
66
+ refProject = projects.find((p) => p.dir === options.ref || p.name === options.ref)
67
+ if (!refProject) {
68
+ console.error(Color.red(`Reference project not found: ${options.ref}`))
69
+ process.exit(1)
70
+ }
71
+ }
72
+
73
+ const allScriptNames = new Set()
74
+ for (const p of projects) {
75
+ for (const key of Object.keys(p.scripts)) {
76
+ if (!filterScripts.length || filterScripts.includes(key)) {
77
+ allScriptNames.add(key)
78
+ }
79
+ }
80
+ }
81
+
82
+ let printed = 0
83
+
84
+ for (const scriptName of [...allScriptNames].sort()) {
85
+ const withScript = projects.filter((p) => p.scripts[scriptName] !== undefined)
86
+ if (withScript.length < 2 && !refProject) continue
87
+
88
+ const expectedValue = refProject ? refProject.scripts[scriptName] : mostCommon(withScript.map((p) => p.scripts[scriptName]))
89
+
90
+ const allMatch = withScript.every((p) => p.scripts[scriptName] === expectedValue) && (!refProject || projects.every((p) => p.scripts[scriptName] !== undefined))
91
+
92
+ if (!options.all && allMatch) continue
93
+
94
+ console.log(`\n${Color.bold(scriptName)}`)
95
+
96
+ if (options.flat) {
97
+ // Per-project lines
98
+ const allRelevant = refProject ? projects : withScript
99
+ for (const p of allRelevant) {
100
+ const value = p.scripts[scriptName]
101
+ const missing = value === undefined
102
+ const matches = !missing && value === expectedValue
103
+ const icon = matches ? Color.green('✓') : Color.red('✗')
104
+ const label = matches ? Color.faint(p.dir) : Color.bold(p.dir)
105
+ const display = missing ? Color.faint('(missing)') : Color.faint(value)
106
+ console.log(` ${icon} ${label} ${display}`)
107
+ }
108
+ } else {
109
+ // Grouped by value
110
+ const groups = new Map()
111
+ const allRelevant = refProject ? projects : withScript
112
+
113
+ for (const p of allRelevant) {
114
+ const value = p.scripts[scriptName] ?? null
115
+ if (!groups.has(value)) groups.set(value, [])
116
+ groups.get(value).push(p.dir)
117
+ }
118
+
119
+ // Sort: expected value first, then others, missing last
120
+ const sorted = [...groups.entries()].sort(([a], [b]) => {
121
+ if (a === expectedValue) return -1
122
+ if (b === expectedValue) return 1
123
+ if (a === null) return 1
124
+ if (b === null) return -1
125
+ return 0
126
+ })
127
+
128
+ for (const [value, dirs] of sorted) {
129
+ const matches = value === expectedValue
130
+ const icon = matches ? Color.green('✓') : Color.red('✗')
131
+ const label = matches ? Color.faint(dirs.join(', ')) : Color.bold(dirs.join(', '))
132
+ const display = value === null ? Color.faint('(missing)') : Color.faint(value)
133
+ console.log(` ${icon} ${label} ${display}`)
134
+ }
135
+ }
136
+
137
+ printed++
138
+ }
139
+
140
+ if (!printed) {
141
+ console.log(Color.green('\nAll scripts are consistent across projects.'))
142
+ } else {
143
+ console.log()
144
+ }
145
+ })
@@ -1,7 +1,7 @@
1
- import { extname, basename } from 'node:path'
2
1
  import { readdirSync, rmSync, statSync } from 'node:fs'
3
- import { createInterface } from 'node:readline'
2
+ import { basename, extname } from 'node:path'
4
3
  import { join, resolve } from 'node:path'
4
+ import { createInterface } from 'node:readline'
5
5
 
6
6
  import { Color, Program, Spinner } from 'termkit'
7
7
 
@@ -1,4 +1,4 @@
1
- import { readFileSync, readdirSync, statSync } from 'node:fs'
1
+ import { readdirSync, readFileSync, statSync } from 'node:fs'
2
2
  import { homedir } from 'node:os'
3
3
  import { join, resolve } from 'node:path'
4
4
 
@@ -77,16 +77,12 @@ export const command = Program.command('find-dep', '[deps...]')
77
77
  console.log(Color.bold(`\nFound ${results.length} project${results.length !== 1 ? 's' : ''}:\n`))
78
78
 
79
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)
80
+ const label = result.projectName && result.projectName !== result.dir ? `${Color.bold(result.dir)} ${Color.faint(`(${result.projectName})`)}` : Color.bold(result.dir)
83
81
 
84
82
  console.log(` ${label}`)
85
83
 
86
84
  for (const dep of result.found) {
87
- const fieldLabel = dep.field === 'dependencies' ? 'dep'
88
- : dep.field === 'devDependencies' ? 'dev'
89
- : 'peer'
85
+ const fieldLabel = dep.field === 'dependencies' ? 'dep' : dep.field === 'devDependencies' ? 'dev' : 'peer'
90
86
  console.log(` ${Color.cyan(dep.name)} ${Color.faint(`${dep.version} [${fieldLabel}]`)}`)
91
87
  }
92
88
  }
@@ -0,0 +1,81 @@
1
+ import { readdirSync, readFileSync, 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
+ function findMatches(pkgPath, command) {
8
+ let pkg
9
+ try {
10
+ pkg = JSON.parse(readFileSync(pkgPath, 'utf8'))
11
+ } catch {
12
+ return null
13
+ }
14
+
15
+ if (!pkg.scripts) return null
16
+
17
+ const found = Object.entries(pkg.scripts)
18
+ .filter(([, value]) => value === command)
19
+ .map(([key]) => key)
20
+
21
+ return found.length ? { projectName: pkg.name, found } : null
22
+ }
23
+
24
+ export const command = Program.command('find-script', '<command>')
25
+ .description('Find projects whose package.json scripts contain an exact command value')
26
+ .option('d', 'dir', '[dir]', 'Root directory to scan (default: ~/Developer)')
27
+ .action(async (options) => {
28
+ const target = options.command
29
+
30
+ const root = resolve(options.dir ?? join(homedir(), 'Developer'))
31
+
32
+ let entries
33
+ try {
34
+ entries = readdirSync(root)
35
+ } catch {
36
+ console.error(Color.red(`Could not read directory: ${root}`))
37
+ process.exit(1)
38
+ }
39
+
40
+ const spinner = new Spinner({ text: 'Scanning projects...' })
41
+ spinner.start()
42
+
43
+ const results = []
44
+
45
+ for (const name of entries) {
46
+ const dir = join(root, name)
47
+ try {
48
+ if (!statSync(dir).isDirectory()) continue
49
+ } catch {
50
+ continue
51
+ }
52
+
53
+ spinner.message(name)
54
+ const match = findMatches(join(dir, 'package.json'), target)
55
+ if (match) results.push({ dir: name, ...match })
56
+ }
57
+
58
+ if (!results.length) {
59
+ spinner.succeed(`No projects found with script: ${target}`)
60
+ return
61
+ }
62
+
63
+ spinner.stop()
64
+
65
+ console.log(Color.bold(`\nFound ${results.length} project${results.length !== 1 ? 's' : ''}:\n`))
66
+
67
+ for (const result of results) {
68
+ const label =
69
+ result.projectName && result.projectName !== result.dir
70
+ ? `${Color.bold(result.dir)} ${Color.faint(`(${result.projectName})`)}`
71
+ : Color.bold(result.dir)
72
+
73
+ console.log(` ${label}`)
74
+
75
+ for (const key of result.found) {
76
+ console.log(` ${Color.cyan(key)}`)
77
+ }
78
+ }
79
+
80
+ console.log()
81
+ })
@@ -1,4 +1,5 @@
1
1
  import { readdirSync, statSync } from 'node:fs'
2
+ import { homedir } from 'node:os'
2
3
  import { join, resolve } from 'node:path'
3
4
 
4
5
  import { Color, Program, Spinner } from 'termkit'
@@ -32,7 +33,7 @@ export const command = Program.command('folder-sizes')
32
33
  .description('List folders sorted by size, largest first')
33
34
  .variable('[dir]')
34
35
  .action(async (args) => {
35
- const root = resolve(args.dir ?? '.')
36
+ const root = resolve(args.dir ?? join(homedir(), 'Developer'))
36
37
 
37
38
  let entries
38
39
  try {
@@ -0,0 +1,61 @@
1
+ import { existsSync, 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
+ export const command = Program.command('including', '[file]')
8
+ .description('Find projects in a directory that contain a given file')
9
+ .option('d', 'dir', '[dir]', 'Root directory to scan (default: ~/Developer)')
10
+ .action(async (options) => {
11
+ const file = options.file
12
+
13
+ if (!file) {
14
+ console.error(Color.red('Provide a file name to search for.'))
15
+ process.exit(1)
16
+ }
17
+
18
+ const root = resolve(options.dir ?? join(homedir(), 'Developer'))
19
+
20
+ let entries
21
+ try {
22
+ entries = readdirSync(root)
23
+ } catch {
24
+ console.error(Color.red(`Could not read directory: ${root}`))
25
+ process.exit(1)
26
+ }
27
+
28
+ const spinner = new Spinner({ text: 'Scanning projects...' })
29
+ spinner.start()
30
+
31
+ const found = []
32
+
33
+ for (const name of entries) {
34
+ const dir = join(root, name)
35
+ try {
36
+ if (!statSync(dir).isDirectory()) continue
37
+ } catch {
38
+ continue
39
+ }
40
+
41
+ spinner.message(name)
42
+
43
+ if (!existsSync(join(dir, 'package.json'))) continue
44
+ if (existsSync(join(dir, file))) found.push(name)
45
+ }
46
+
47
+ if (!found.length) {
48
+ spinner.succeed(`No projects found containing: ${file}`)
49
+ return
50
+ }
51
+
52
+ spinner.stop()
53
+
54
+ console.log(Color.bold(`\n${found.length} project${found.length !== 1 ? 's' : ''} containing ${Color.cyan(file)}:\n`))
55
+
56
+ for (const name of found) {
57
+ console.log(` ${Color.green(name)}`)
58
+ }
59
+
60
+ console.log()
61
+ })
@@ -0,0 +1,61 @@
1
+ import { existsSync, 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
+ export const command = Program.command('missing', '[file]')
8
+ .description('Find projects in a directory that are missing a given file')
9
+ .option('d', 'dir', '[dir]', 'Root directory to scan (default: ~/Developer)')
10
+ .action(async (options) => {
11
+ const file = options.file
12
+
13
+ if (!file) {
14
+ console.error(Color.red('Provide a file name to search for.'))
15
+ process.exit(1)
16
+ }
17
+
18
+ const root = resolve(options.dir ?? join(homedir(), 'Developer'))
19
+
20
+ let entries
21
+ try {
22
+ entries = readdirSync(root)
23
+ } catch {
24
+ console.error(Color.red(`Could not read directory: ${root}`))
25
+ process.exit(1)
26
+ }
27
+
28
+ const spinner = new Spinner({ text: 'Scanning projects...' })
29
+ spinner.start()
30
+
31
+ const missing = []
32
+
33
+ for (const name of entries) {
34
+ const dir = join(root, name)
35
+ try {
36
+ if (!statSync(dir).isDirectory()) continue
37
+ } catch {
38
+ continue
39
+ }
40
+
41
+ spinner.message(name)
42
+
43
+ if (!existsSync(join(dir, 'package.json'))) continue
44
+ if (!existsSync(join(dir, file))) missing.push(name)
45
+ }
46
+
47
+ if (!missing.length) {
48
+ spinner.succeed(`All projects contain: ${file}`)
49
+ return
50
+ }
51
+
52
+ spinner.stop()
53
+
54
+ console.log(Color.bold(`\n${missing.length} project${missing.length !== 1 ? 's' : ''} missing ${Color.cyan(file)}:\n`))
55
+
56
+ for (const name of missing) {
57
+ console.log(` ${Color.yellow(name)}`)
58
+ }
59
+
60
+ console.log()
61
+ })
@@ -3,7 +3,7 @@ import { cpSync, existsSync, readFileSync, rmSync, writeFileSync } from 'node:fs
3
3
  import { homedir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
6
- import { Color, Program, log } from 'termkit'
6
+ import { Color, log, Program } from 'termkit'
7
7
 
8
8
  const BOILERPLATE_REPO = 'git@github.com:jayrdeaton/Expo-Boilerplate.git'
9
9
  const BOILERPLATE_DIR = join(homedir(), 'Developer', 'Expo-Boilerplate')
@@ -49,7 +49,7 @@ export const command = Program.command('new-expo-project')
49
49
  log.info(`Creating ${displayName}...`)
50
50
  cpSync(BOILERPLATE_DIR, targetDir, {
51
51
  recursive: true,
52
- filter: (src) => !src.includes('/node_modules/'),
52
+ filter: (src) => !src.includes('/node_modules/')
53
53
  })
54
54
 
55
55
  rmSync(join(targetDir, '.git'), { recursive: true, force: true })
@@ -1,7 +1,7 @@
1
1
  import { readdirSync, renameSync, statSync } from 'node:fs'
2
2
  import { extname, join } from 'node:path'
3
3
 
4
- import { Color, Program, log } from 'termkit'
4
+ import { Color, log, Program } from 'termkit'
5
5
 
6
6
  export const command = Program.command('rename-season')
7
7
  .description('Rename files in a directory to SxEE format for TV library pickup')
@@ -0,0 +1,73 @@
1
+ import { execSync } from 'node:child_process'
2
+
3
+ import { Color, Program, Spinner } from 'termkit'
4
+
5
+ function gh(path) {
6
+ const out = execSync(`gh api "${path}" --paginate`, {
7
+ encoding: 'utf8',
8
+ stdio: ['ignore', 'pipe', 'ignore']
9
+ })
10
+ return JSON.parse(out)
11
+ }
12
+
13
+ export const command = Program.command('repo-rulesets')
14
+ .description('Find public repos that have no ruleset attached')
15
+ .option('u', 'user', '<user>', 'GitHub username (defaults to authenticated user)')
16
+ .action(async (options) => {
17
+ const spinner = new Spinner({ text: 'Resolving user...' })
18
+ spinner.start()
19
+
20
+ let user = options.user
21
+ if (!user) {
22
+ try {
23
+ const data = gh('/user')
24
+ user = data.login
25
+ } catch {
26
+ spinner.fail('Could not resolve GitHub user. Run `gh auth login` or pass --user.')
27
+ process.exit(1)
28
+ }
29
+ }
30
+
31
+ spinner.message(`Fetching public repos for ${user}...`)
32
+
33
+ let repos
34
+ try {
35
+ repos = gh(`/users/${user}/repos?type=public&per_page=100`)
36
+ } catch {
37
+ spinner.fail(`Could not fetch repos for ${user}.`)
38
+ process.exit(1)
39
+ }
40
+
41
+ if (!repos.length) {
42
+ spinner.succeed(`No public repos found for ${user}.`)
43
+ return
44
+ }
45
+
46
+ spinner.message(`Checking rulesets across ${repos.length} repos...`)
47
+
48
+ const missing = []
49
+
50
+ for (const repo of repos) {
51
+ spinner.message(repo.name)
52
+ try {
53
+ const rulesets = gh(`/repos/${user}/${repo.name}/rulesets`)
54
+ if (!rulesets.length) missing.push(repo.name)
55
+ } catch {
56
+ // API error means no access or rulesets not available — treat as missing
57
+ missing.push(repo.name)
58
+ }
59
+ }
60
+
61
+ if (!missing.length) {
62
+ spinner.succeed(`All ${repos.length} public repos have a ruleset.`)
63
+ return
64
+ }
65
+
66
+ spinner.stop()
67
+
68
+ console.log(Color.bold.yellow(`\nNo ruleset (${missing.length} of ${repos.length})\n`))
69
+ for (const name of missing) {
70
+ console.log(` ${Color.yellow(name)}`)
71
+ }
72
+ console.log()
73
+ })
@@ -1,5 +1,6 @@
1
1
  import { execSync } from 'node:child_process'
2
2
  import { readdirSync, statSync } from 'node:fs'
3
+ import { homedir } from 'node:os'
3
4
  import { join, resolve } from 'node:path'
4
5
 
5
6
  import { Color, Program, Spinner } from 'termkit'
@@ -37,7 +38,7 @@ export const command = Program.command('repo-status')
37
38
  .description('Report dirty and untracked files across repos in a directory')
38
39
  .variable('[dir]')
39
40
  .action(async (args) => {
40
- const root = resolve(args.dir ?? '.')
41
+ const root = resolve(args.dir ?? join(homedir(), 'Developer'))
41
42
 
42
43
  let entries
43
44
  try {
@@ -0,0 +1,166 @@
1
+ import { execSync } from 'node:child_process'
2
+ import { readdirSync, readFileSync, statSync, writeFileSync } from 'node:fs'
3
+ import { homedir } from 'node:os'
4
+ import { join, resolve } from 'node:path'
5
+
6
+ import { Color, log, Program } from 'termkit'
7
+
8
+ const NON_SEMVER = /^(file:|link:|workspace:|git\+|github:|https?:|\/)/
9
+
10
+ const RUNTIME_PATTERNS = [/^react$/, /^react-dom$/, /^react-native/, /^expo(-|$)/, /^@expo\//, /^@gorhom\//, /^@shopify\//, /^@reduxjs\//]
11
+
12
+ function isRuntimeDep(name) {
13
+ return RUNTIME_PATTERNS.some((p) => p.test(name))
14
+ }
15
+
16
+ function stripRange(raw) {
17
+ return raw.replace(/^[\^~>=<\s]+/, '').trim()
18
+ }
19
+
20
+ function toFloor(raw) {
21
+ if (NON_SEMVER.test(raw.trim())) return null
22
+ const clean = stripRange(raw)
23
+ if (!/^\d/.test(clean)) return null
24
+ const parts = clean.split('.')
25
+ const major = parseInt(parts[0], 10) || 0
26
+ const minor = parseInt(parts[1], 10) || 0
27
+ return `>=${major}.${minor}.0`
28
+ }
29
+
30
+ function isSemver(raw) {
31
+ return !NON_SEMVER.test(raw.trim()) && /^\d/.test(stripRange(raw))
32
+ }
33
+
34
+ function sortedDeps(deps) {
35
+ return Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b)))
36
+ }
37
+
38
+ function readPkg(dir) {
39
+ try {
40
+ return JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
41
+ } catch {
42
+ return null
43
+ }
44
+ }
45
+
46
+ export const command = Program.command('sync-peers')
47
+ .description('Sync @rific peerDependency floors and devDependency versions to match Expo-Starter')
48
+ .option('s', 'starter', '[starter]', 'Path to Expo-Starter project (default: ~/Developer/Expo-Starter)')
49
+ .option('r', 'root', '[root]', 'Root directory containing @rific packages (default: ~/Developer)')
50
+ .option('d', 'dry', null, 'Preview changes without writing')
51
+ .option('i', 'install', null, 'Run npm install in each changed repo')
52
+ .option('t', 'test', null, 'Run npm test in each changed repo (implies --install)')
53
+ .action(async (options) => {
54
+ const starterPath = resolve(options.starter ?? join(homedir(), 'Developer', 'Expo-Starter'))
55
+ const rootPath = resolve(options.root ?? join(homedir(), 'Developer'))
56
+ const isDry = !!options.dry
57
+ const shouldTest = !!options.test
58
+ const shouldInstall = !!options.install || shouldTest
59
+
60
+ const starterPkg = readPkg(starterPath)
61
+ if (!starterPkg) {
62
+ log.fail(`No package.json found at: ${starterPath}`)
63
+ process.exit(1)
64
+ }
65
+
66
+ const starterDeps = { ...starterPkg.dependencies, ...starterPkg.devDependencies }
67
+ const expoVersion = starterDeps.expo ? stripRange(starterDeps.expo) : 'unknown'
68
+ log.info(`Expo SDK floor: ${expoVersion}`)
69
+
70
+ const entries = readdirSync(rootPath).filter((name) => {
71
+ try {
72
+ return statSync(join(rootPath, name)).isDirectory()
73
+ } catch {
74
+ return false
75
+ }
76
+ })
77
+
78
+ const rificPkgs = entries
79
+ .map((name) => {
80
+ const dir = join(rootPath, name)
81
+ const pkg = readPkg(dir)
82
+ if (!pkg?.name?.startsWith('@rific/')) return null
83
+ return { name, dir, pkg }
84
+ })
85
+ .filter(Boolean)
86
+ .sort((a, b) => a.name.localeCompare(b.name))
87
+
88
+ log.info(`Found ${rificPkgs.length} @rific packages\n`)
89
+
90
+ let totalChanges = 0
91
+
92
+ for (const { name, dir, pkg } of rificPkgs) {
93
+ const peerUpdates = {}
94
+ const devUpdates = {}
95
+
96
+ for (const [dep, current] of Object.entries(pkg.peerDependencies ?? {})) {
97
+ if (!(dep in starterDeps)) continue
98
+ const floor = toFloor(starterDeps[dep])
99
+ if (!floor || floor === current) continue
100
+ peerUpdates[dep] = { from: current, to: floor }
101
+ }
102
+
103
+ for (const [dep, current] of Object.entries(pkg.devDependencies ?? {})) {
104
+ if (!isRuntimeDep(dep) || !(dep in starterDeps)) continue
105
+ const next = starterDeps[dep]
106
+ if (!isSemver(next) || next === current) continue
107
+ devUpdates[dep] = { from: current, to: next }
108
+ }
109
+
110
+ const hasChanges = Object.keys(peerUpdates).length > 0 || Object.keys(devUpdates).length > 0
111
+
112
+ if (!hasChanges) {
113
+ console.log(`${Color.bold(name)} ${Color.faint('no changes')}`)
114
+ continue
115
+ }
116
+
117
+ console.log(Color.bold(name))
118
+
119
+ if (Object.keys(peerUpdates).length > 0) {
120
+ console.log(Color.faint(' peerDependencies'))
121
+ for (const [dep, { from, to }] of Object.entries(peerUpdates)) {
122
+ console.log(` ${dep}`)
123
+ console.log(` ${Color.red(from)} → ${Color.green(to)}`)
124
+ totalChanges++
125
+ }
126
+ }
127
+
128
+ if (Object.keys(devUpdates).length > 0) {
129
+ console.log(Color.faint(' devDependencies'))
130
+ for (const [dep, { from, to }] of Object.entries(devUpdates)) {
131
+ console.log(` ${dep}`)
132
+ console.log(` ${Color.red(from)} → ${Color.green(to)}`)
133
+ totalChanges++
134
+ }
135
+ }
136
+
137
+ if (!isDry) {
138
+ const updated = { ...pkg }
139
+ if (Object.keys(peerUpdates).length > 0) {
140
+ updated.peerDependencies = sortedDeps({ ...pkg.peerDependencies, ...Object.fromEntries(Object.entries(peerUpdates).map(([d, { to }]) => [d, to])) })
141
+ }
142
+ if (Object.keys(devUpdates).length > 0) {
143
+ updated.devDependencies = sortedDeps({ ...pkg.devDependencies, ...Object.fromEntries(Object.entries(devUpdates).map(([d, { to }]) => [d, to])) })
144
+ }
145
+ writeFileSync(join(dir, 'package.json'), JSON.stringify(updated, null, 2) + '\n')
146
+ if (shouldInstall) {
147
+ console.log(Color.faint(' npm install'))
148
+ execSync('npm install', { cwd: dir, stdio: 'inherit' })
149
+ }
150
+ if (shouldTest) {
151
+ console.log(Color.faint(' npm test'))
152
+ execSync('npm test', { cwd: dir, stdio: 'inherit' })
153
+ }
154
+ }
155
+
156
+ console.log()
157
+ }
158
+
159
+ if (totalChanges === 0) {
160
+ log.succeed('All dependencies already aligned.')
161
+ } else if (isDry) {
162
+ log.info(`${totalChanges} change${totalChanges !== 1 ? 's' : ''} pending — run without --dry to apply`)
163
+ } else {
164
+ log.succeed(`Applied ${totalChanges} change${totalChanges !== 1 ? 's' : ''}.`)
165
+ }
166
+ })
@@ -3,7 +3,7 @@ import { existsSync } from 'node:fs'
3
3
  import { homedir } from 'node:os'
4
4
  import { join } from 'node:path'
5
5
 
6
- import { Color, Program, log } from 'termkit'
6
+ import { Color, log, Program } from 'termkit'
7
7
 
8
8
  const BOILERPLATE_REPO = 'git@github.com:jayrdeaton/Expo-Boilerplate.git'
9
9
  const BOILERPLATE_DIR = join(homedir(), 'Developer', 'Expo-Boilerplate')
@@ -2,7 +2,7 @@ import { execSync } from 'node:child_process'
2
2
  import { readFileSync } from 'node:fs'
3
3
  import { resolve } from 'node:path'
4
4
 
5
- import { Color, Program, log } from 'termkit'
5
+ import { Color, log, Program } from 'termkit'
6
6
 
7
7
  function exec(cmd) {
8
8
  console.log(Color.faint(`$ ${cmd}`))
@@ -0,0 +1,95 @@
1
+ import { existsSync, readdirSync, readFileSync, 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 findYalcDeps(pkgPath) {
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 [name, version] of Object.entries(pkg[field])) {
21
+ if (typeof version === 'string' && version.startsWith('file:.yalc/')) {
22
+ found.push({ name, version, field })
23
+ }
24
+ }
25
+ }
26
+
27
+ return { projectName: pkg.name, found }
28
+ }
29
+
30
+ export const command = Program.command('yalc-check')
31
+ .description('Find projects in a directory that have yalc dependencies')
32
+ .option('d', 'dir', '[dir]', 'Root directory to scan (default: ~/Developer)')
33
+ .action(async (options) => {
34
+ const root = resolve(options.dir ?? join(homedir(), 'Developer'))
35
+
36
+ let entries
37
+ try {
38
+ entries = readdirSync(root)
39
+ } catch {
40
+ console.error(Color.red(`Could not read directory: ${root}`))
41
+ process.exit(1)
42
+ }
43
+
44
+ const spinner = new Spinner({ text: 'Scanning projects...' })
45
+ spinner.start()
46
+
47
+ const results = []
48
+
49
+ for (const name of entries) {
50
+ const dir = join(root, name)
51
+ try {
52
+ if (!statSync(dir).isDirectory()) continue
53
+ } catch {
54
+ continue
55
+ }
56
+
57
+ spinner.message(name)
58
+
59
+ const pkgPath = join(dir, 'package.json')
60
+ if (!existsSync(pkgPath)) continue
61
+
62
+ const hasLock = existsSync(join(dir, 'yalc.lock'))
63
+ const { projectName, found } = findYalcDeps(pkgPath)
64
+
65
+ if (found.length || hasLock) {
66
+ results.push({ dir: name, projectName, found, hasLock })
67
+ }
68
+ }
69
+
70
+ if (!results.length) {
71
+ spinner.succeed('No projects with yalc dependencies found')
72
+ return
73
+ }
74
+
75
+ spinner.stop()
76
+
77
+ console.log(Color.bold(`\n${results.length} project${results.length !== 1 ? 's' : ''} with yalc dependencies:\n`))
78
+
79
+ for (const result of results) {
80
+ const label =
81
+ result.projectName && result.projectName !== result.dir
82
+ ? `${Color.bold(result.dir)} ${Color.faint(`(${result.projectName})`)}`
83
+ : Color.bold(result.dir)
84
+
85
+ const lockNote = result.hasLock && !result.found.length ? Color.yellow(' yalc.lock present') : ''
86
+ console.log(` ${label}${lockNote}`)
87
+
88
+ for (const dep of result.found) {
89
+ const fieldLabel = dep.field === 'dependencies' ? 'dep' : dep.field === 'devDependencies' ? 'dev' : 'peer'
90
+ console.log(` ${Color.cyan(dep.name)} ${Color.faint(`${dep.version} [${fieldLabel}]`)}`)
91
+ }
92
+ }
93
+
94
+ console.log()
95
+ })