@jayrdeaton/scripts 1.1.2 → 1.1.4
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 +89 -1
- package/package.json +1 -1
- package/src/commands/check-scripts.mjs +2 -6
- package/src/commands/find-script.mjs +78 -0
- package/src/commands/folder-sizes.mjs +2 -1
- package/src/commands/including.mjs +61 -0
- package/src/commands/missing.mjs +61 -0
- package/src/commands/repo-rulesets.mjs +73 -0
- package/src/commands/repo-status.mjs +2 -1
- package/src/commands/sync-peers.mjs +166 -0
- package/src/commands/update-deps.mjs +3 -1
- package/src/commands/yalc-check.mjs +92 -0
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.
|
|
@@ -246,7 +321,7 @@ jrd update-boilerplate
|
|
|
246
321
|
|
|
247
322
|
### `jrd update-deps`
|
|
248
323
|
|
|
249
|
-
Update all npm dependencies to `@latest`. Automatically runs `npx expo install --fix` if the project uses Expo.
|
|
324
|
+
Update all npm dependencies to `@latest`. Automatically runs `npx expo install --fix` if the project uses Expo. Skips any dependency whose version is not a plain version number — `file:`, `yalc:`, `link:`, `workspace:`, and similar non-registry entries are left untouched.
|
|
250
325
|
|
|
251
326
|
```
|
|
252
327
|
jrd update-deps [options]
|
|
@@ -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
|
@@ -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,78 @@
|
|
|
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 = result.projectName && result.projectName !== result.dir ? `${Color.bold(result.dir)} ${Color.faint(`(${result.projectName})`)}` : Color.bold(result.dir)
|
|
69
|
+
|
|
70
|
+
console.log(` ${label}`)
|
|
71
|
+
|
|
72
|
+
for (const key of result.found) {
|
|
73
|
+
console.log(` ${Color.cyan(key)}`)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log()
|
|
78
|
+
})
|
|
@@ -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
|
+
})
|
|
@@ -10,7 +10,9 @@ function exec(cmd) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function latestPackages(deps = {}) {
|
|
13
|
-
return Object.
|
|
13
|
+
return Object.entries(deps)
|
|
14
|
+
.filter(([, version]) => /^\^?[\d*]/.test(version))
|
|
15
|
+
.map(([name]) => `${name}@latest`)
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export const command = Program.command('update-deps')
|
|
@@ -0,0 +1,92 @@
|
|
|
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 = result.projectName && result.projectName !== result.dir ? `${Color.bold(result.dir)} ${Color.faint(`(${result.projectName})`)}` : Color.bold(result.dir)
|
|
81
|
+
|
|
82
|
+
const lockNote = result.hasLock && !result.found.length ? Color.yellow(' yalc.lock present') : ''
|
|
83
|
+
console.log(` ${label}${lockNote}`)
|
|
84
|
+
|
|
85
|
+
for (const dep of result.found) {
|
|
86
|
+
const fieldLabel = dep.field === 'dependencies' ? 'dep' : dep.field === 'devDependencies' ? 'dev' : 'peer'
|
|
87
|
+
console.log(` ${Color.cyan(dep.name)} ${Color.faint(`${dep.version} [${fieldLabel}]`)}`)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log()
|
|
92
|
+
})
|