@jayrdeaton/scripts 1.1.2 → 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
@@ -149,6 +149,21 @@ Example: `jrd find-dep react-native expo`
149
149
 
150
150
  ---
151
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
+
152
167
  ### `jrd focus`
153
168
 
154
169
  Bring an application to the front using AppleScript.
@@ -161,6 +176,21 @@ Default app is `Terminal`.
161
176
 
162
177
  ---
163
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
+
164
194
  ### `jrd folder-sizes`
165
195
 
166
196
  List all subdirectories sorted by size, largest first.
@@ -171,6 +201,21 @@ jrd folder-sizes [dir]
171
201
 
172
202
  ---
173
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
+
174
219
  ### `jrd new-expo-project`
175
220
 
176
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.
@@ -224,6 +269,19 @@ jrd rename-season <season> [dir]
224
269
 
225
270
  ---
226
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
+
227
285
  ### `jrd repo-status`
228
286
 
229
287
  Scan a directory of git repos and report which ones have dirty files, untracked files, or unpushed commits.
@@ -234,6 +292,23 @@ jrd repo-status [dir]
234
292
 
235
293
  ---
236
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
+
237
312
  ### `jrd update-boilerplate`
238
313
 
239
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.
@@ -257,6 +332,19 @@ Options:
257
332
  -l, --legacy Pass --legacy-peer-deps to npm install
258
333
  ```
259
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
+
260
348
  ## Requirements
261
349
 
262
350
  Node >= 20
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jayrdeaton/scripts",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "private": false,
5
5
  "description": "Personal dev scripts",
6
6
  "repository": {
@@ -85,13 +85,9 @@ export const command = Program.command('check-scripts', '[scripts...]')
85
85
  const withScript = projects.filter((p) => p.scripts[scriptName] !== undefined)
86
86
  if (withScript.length < 2 && !refProject) continue
87
87
 
88
- const expectedValue = refProject
89
- ? refProject.scripts[scriptName]
90
- : mostCommon(withScript.map((p) => p.scripts[scriptName]))
88
+ const expectedValue = refProject ? refProject.scripts[scriptName] : mostCommon(withScript.map((p) => p.scripts[scriptName]))
91
89
 
92
- const allMatch =
93
- withScript.every((p) => p.scripts[scriptName] === expectedValue) &&
94
- (!refProject || projects.every((p) => p.scripts[scriptName] !== undefined))
90
+ const allMatch = withScript.every((p) => p.scripts[scriptName] === expectedValue) && (!refProject || projects.every((p) => p.scripts[scriptName] !== undefined))
95
91
 
96
92
  if (!options.all && allMatch) continue
97
93
 
@@ -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
+ })
@@ -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
+ })
@@ -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
+ })