@o/scripts 0.16.19-canary.1790165522484

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/src/up.ts ADDED
@@ -0,0 +1,440 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { cmd } from './cmd'
4
+
5
+ interface PackageJson {
6
+ dependencies?: Record<string, string>
7
+ devDependencies?: Record<string, string>
8
+ peerDependencies?: Record<string, string>
9
+ optionalDependencies?: Record<string, string>
10
+ }
11
+
12
+ await cmd`upgrade packages by name or pattern`
13
+ .args('--tag string --canary boolean --rc boolean')
14
+ .run(async ({ args, $, path, fs }) => {
15
+ let globalTag: string | undefined = args.tag
16
+ if (args.canary) globalTag = 'canary'
17
+ if (args.rc) globalTag = 'rc'
18
+
19
+ const packagePatterns: string[] = []
20
+ const rootDir = process.cwd()
21
+ const rootPackageJson = JSON.parse(
22
+ fs.readFileSync(path.join(rootDir, 'package.json'), 'utf-8')
23
+ )
24
+ const upgradeSets: Record<string, string[]> = rootPackageJson.upgradeSets || {}
25
+
26
+ for (const arg of args.rest) {
27
+ if (arg in upgradeSets) {
28
+ // expand named upgrade set to its patterns
29
+ packagePatterns.push(...upgradeSets[arg]!)
30
+ } else {
31
+ packagePatterns.push(arg)
32
+ }
33
+ }
34
+
35
+ if (packagePatterns.length === 0) {
36
+ const setNames = Object.keys(upgradeSets)
37
+ if (setNames.length > 0) {
38
+ console.info('Usage: bun tko up <target|pattern> [options]')
39
+ console.info(`\nAvailable upgrade sets: ${setNames.join(', ')}`)
40
+ console.info('\nOr provide package patterns directly:')
41
+ console.info(' bun tko up @vxrn/* vxrn')
42
+ console.info(' bun tko up --tag canary react react-dom')
43
+ } else {
44
+ console.error('Please provide at least one package pattern to update.')
45
+ console.error('Example: bun tko up @vxrn/* vxrn')
46
+ console.error('Or with a tag: bun tko up --tag canary @vxrn/* vxrn')
47
+ }
48
+ process.exit(1)
49
+ }
50
+
51
+ function findPackageJsonFiles(dir: string): string[] {
52
+ const results: string[] = []
53
+
54
+ if (fs.existsSync(path.join(dir, 'package.json'))) {
55
+ results.push(path.join(dir, 'package.json'))
56
+ }
57
+
58
+ // check if it's a monorepo with workspaces
59
+ try {
60
+ const packageJson = JSON.parse(
61
+ fs.readFileSync(path.join(dir, 'package.json'), 'utf-8')
62
+ )
63
+ if (packageJson.workspaces) {
64
+ let workspacePaths: string[] = []
65
+
66
+ if (Array.isArray(packageJson.workspaces)) {
67
+ workspacePaths = packageJson.workspaces
68
+ } else if (packageJson.workspaces.packages) {
69
+ workspacePaths = packageJson.workspaces.packages
70
+ }
71
+
72
+ for (const workspace of workspacePaths) {
73
+ // handle glob patterns like "packages/*", "code/**/*", "./code/ui/**/*"
74
+ const normalizedWorkspace = workspace.replace(/^\.\//, '')
75
+
76
+ if (normalizedWorkspace.includes('**')) {
77
+ // nested glob pattern - use glob to find all package.json files
78
+ const baseDir = normalizedWorkspace.split('**')[0]!.replace(/\/$/, '')
79
+ const basePath = path.join(dir, baseDir)
80
+
81
+ if (fs.existsSync(basePath)) {
82
+ const findPackages = (searchDir: string) => {
83
+ try {
84
+ const entries = fs.readdirSync(searchDir, { withFileTypes: true })
85
+ for (const entry of entries) {
86
+ if (entry.isDirectory() && entry.name !== 'node_modules') {
87
+ const subPath = path.join(searchDir, entry.name)
88
+ const pkgPath = path.join(subPath, 'package.json')
89
+ if (fs.existsSync(pkgPath)) {
90
+ results.push(pkgPath)
91
+ }
92
+ // recurse into subdirectories
93
+ findPackages(subPath)
94
+ }
95
+ }
96
+ } catch (_e) {
97
+ // ignore permission errors
98
+ }
99
+ }
100
+ findPackages(basePath)
101
+ }
102
+ } else if (normalizedWorkspace.includes('*')) {
103
+ // simple glob pattern like "packages/*"
104
+ const workspaceDir = normalizedWorkspace.replace(/\/\*$/, '')
105
+ if (fs.existsSync(path.join(dir, workspaceDir))) {
106
+ const subdirs = fs
107
+ .readdirSync(path.join(dir, workspaceDir), {
108
+ withFileTypes: true,
109
+ })
110
+ .filter((dirent) => dirent.isDirectory())
111
+ .map((dirent) => path.join(dir, workspaceDir, dirent.name))
112
+
113
+ for (const subdir of subdirs) {
114
+ if (fs.existsSync(path.join(subdir, 'package.json'))) {
115
+ results.push(path.join(subdir, 'package.json'))
116
+ }
117
+ }
118
+ }
119
+ } else {
120
+ // exact path like "code/tamagui.dev" or "./code/sandbox"
121
+ const pkgPath = path.join(dir, normalizedWorkspace, 'package.json')
122
+ if (fs.existsSync(pkgPath)) {
123
+ results.push(pkgPath)
124
+ }
125
+ }
126
+ }
127
+ }
128
+ } catch (_error) {
129
+ // ignore errors parsing package.json
130
+ }
131
+
132
+ return results
133
+ }
134
+
135
+ function extractDependencies(packageJsonPath: string): string[] {
136
+ try {
137
+ const content = fs.readFileSync(packageJsonPath, 'utf-8')
138
+ const packageJson = JSON.parse(content) as PackageJson
139
+
140
+ const deps: string[] = []
141
+
142
+ const addNonWorkspaceDeps = (depsObject: Record<string, string> | undefined) => {
143
+ if (!depsObject) return
144
+ for (const [name, version] of Object.entries(depsObject)) {
145
+ // skip workspace dependencies
146
+ if (!version.startsWith('workspace:')) {
147
+ deps.push(name)
148
+ }
149
+ }
150
+ }
151
+
152
+ addNonWorkspaceDeps(packageJson.dependencies)
153
+ addNonWorkspaceDeps(packageJson.devDependencies)
154
+ addNonWorkspaceDeps(packageJson.peerDependencies)
155
+ addNonWorkspaceDeps(packageJson.optionalDependencies)
156
+
157
+ return deps
158
+ } catch (error) {
159
+ console.error(`Error parsing ${packageJsonPath}:`, error)
160
+ return []
161
+ }
162
+ }
163
+
164
+ function doesPackageMatchPattern(packageName: string, pattern: string): boolean {
165
+ if (pattern.includes('*')) {
166
+ const regex = new RegExp(`^${pattern.replace(/\*/g, '.*')}$`)
167
+ return regex.test(packageName)
168
+ }
169
+ return packageName === pattern
170
+ }
171
+
172
+ function updatePackageJsonVersions(
173
+ packageJsonPath: string,
174
+ packagesToUpdate: string[],
175
+ versionMap: Map<string, string>
176
+ ): number {
177
+ const content = fs.readFileSync(packageJsonPath, 'utf-8')
178
+ const packageJson = JSON.parse(content) as PackageJson
179
+ let updatedCount = 0
180
+
181
+ const updateDeps = (depsObject: Record<string, string> | undefined) => {
182
+ if (!depsObject) return
183
+ for (const pkg of packagesToUpdate) {
184
+ const current = depsObject[pkg]
185
+ if (current && !current.startsWith('workspace:')) {
186
+ const newVersion = versionMap.get(pkg)
187
+ if (newVersion) {
188
+ // for tagged versions (canary, rc, etc), use exact version (no prefix)
189
+ // otherwise preserve version prefix (^, ~, >=, etc)
190
+ if (globalTag) {
191
+ depsObject[pkg] = newVersion
192
+ } else {
193
+ // wildcard "*" means newly added placeholder, use ^ prefix
194
+ if (current === '*') {
195
+ depsObject[pkg] = `^${newVersion}`
196
+ updatedCount++
197
+ continue
198
+ }
199
+ const prefixMatch = current.match(/^([^\d]*)/)
200
+ const prefix = prefixMatch?.[1] || ''
201
+ depsObject[pkg] = `${prefix}${newVersion}`
202
+ }
203
+ updatedCount++
204
+ }
205
+ }
206
+ }
207
+ }
208
+
209
+ updateDeps(packageJson.dependencies)
210
+ updateDeps(packageJson.devDependencies)
211
+ updateDeps(packageJson.peerDependencies)
212
+ updateDeps(packageJson.optionalDependencies)
213
+
214
+ if (updatedCount > 0) {
215
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n')
216
+ }
217
+
218
+ return updatedCount
219
+ }
220
+
221
+ async function updatePackages(
222
+ packagesByWorkspace: Map<string, { dir: string; packages: string[] }>,
223
+ rootDir: string,
224
+ packageJsonFiles: string[]
225
+ ) {
226
+ try {
227
+ fs.rmSync(`node_modules/vite`, {
228
+ recursive: true,
229
+ force: true,
230
+ })
231
+ } catch (_e) {
232
+ // ignore if vite is not there
233
+ }
234
+
235
+ // collect all unique packages to update
236
+ const allPackages = new Set<string>()
237
+ for (const { packages } of packagesByWorkspace.values()) {
238
+ packages.forEach((pkg) => allPackages.add(pkg))
239
+ }
240
+
241
+ // fetch versions for all packages (with tag if specified)
242
+ const versionMap = new Map<string, string>()
243
+ console.info(`\nšŸ” Fetching versions for ${allPackages.size} package(s)...`)
244
+
245
+ await Promise.all(
246
+ [...allPackages].map(async (pkg) => {
247
+ try {
248
+ const tag = globalTag || 'latest'
249
+ const result = await $`npm view ${pkg}@${tag} version`.quiet()
250
+ const version = result.text().trim()
251
+ if (version) {
252
+ versionMap.set(pkg, version)
253
+ console.info(` āœ“ ${pkg}@${tag} → ${version}`)
254
+ }
255
+ } catch {
256
+ if (globalTag) {
257
+ console.info(` ⊘ ${pkg}@${globalTag} not found, skipping`)
258
+ } else {
259
+ console.info(` ⊘ ${pkg} not found, skipping`)
260
+ }
261
+ }
262
+ })
263
+ )
264
+
265
+ if (versionMap.size === 0) {
266
+ console.info(`\nāš ļø No packages found to update`)
267
+ return
268
+ }
269
+
270
+ // update all package.json files directly
271
+ console.info(`\nšŸ“¦ Updating ${packageJsonFiles.length} package.json file(s)...`)
272
+ let totalUpdates = 0
273
+
274
+ for (const packageJsonPath of packageJsonFiles) {
275
+ const packagesInWorkspace =
276
+ packagesByWorkspace.get(getWorkspaceName(packageJsonPath, rootDir))?.packages ||
277
+ []
278
+
279
+ if (packagesInWorkspace.length > 0) {
280
+ const updates = updatePackageJsonVersions(
281
+ packageJsonPath,
282
+ packagesInWorkspace,
283
+ versionMap
284
+ )
285
+ if (updates > 0) {
286
+ const name = getWorkspaceName(packageJsonPath, rootDir)
287
+ console.info(` āœ“ ${name}: ${updates} package(s)`)
288
+ totalUpdates += updates
289
+ }
290
+ }
291
+ }
292
+
293
+ console.info(`\nšŸ“ Updated ${totalUpdates} dependency version(s)`)
294
+
295
+ console.info(`\nāš™ļø Running 'bun install'...`)
296
+ $.cwd(rootDir)
297
+ try {
298
+ await $`bun install`
299
+ console.info('āœ… Done!')
300
+ } catch (error: any) {
301
+ const stderr = error.stderr?.toString() || error.message || ''
302
+ // check if it's a version resolution error (common after new publish)
303
+ if (
304
+ stderr.includes('No version matching') ||
305
+ stderr.includes('failed to resolve')
306
+ ) {
307
+ console.info(`āš ļø Version not in cache, clearing cache and retrying...`)
308
+ try {
309
+ await $`bun pm cache rm`.quiet()
310
+ await $`bun install`
311
+ console.info('āœ… Done!')
312
+ } catch (retryError: any) {
313
+ const retryStderr = retryError.stderr?.toString() || retryError.message || ''
314
+ console.error(
315
+ `🚨 'bun install' failed after cache clear: ${retryStderr.split('\n')[0]}`
316
+ )
317
+ }
318
+ } else {
319
+ console.error(`🚨 'bun install' failed: ${stderr.split('\n')[0]}`)
320
+ }
321
+ }
322
+ }
323
+
324
+ function getWorkspaceName(packageJsonPath: string, rootDir: string): string {
325
+ const dir = packageJsonPath.replace('/package.json', '')
326
+ if (dir === rootDir) return 'root'
327
+ return dir.replace(rootDir + '/', '')
328
+ }
329
+
330
+ const packageJsonFiles = findPackageJsonFiles(rootDir)
331
+ console.info(`Found ${packageJsonFiles.length} package.json files`)
332
+
333
+ // get workspace package names to exclude from updates
334
+ const workspacePackageNames = new Set<string>()
335
+ for (const packageJsonPath of packageJsonFiles) {
336
+ if (packageJsonPath === path.join(rootDir, 'package.json')) continue
337
+
338
+ try {
339
+ const content = fs.readFileSync(packageJsonPath, 'utf-8')
340
+ const packageJson = JSON.parse(content)
341
+ if (packageJson.name) {
342
+ workspacePackageNames.add(packageJson.name)
343
+ }
344
+ } catch (_error) {
345
+ // ignore errors
346
+ }
347
+ }
348
+
349
+ console.info(
350
+ `Found ${workspacePackageNames.size} workspace packages to exclude from updates`
351
+ )
352
+
353
+ // build map of packages to update per workspace
354
+ const packagesByWorkspace = new Map<string, { dir: string; packages: string[] }>()
355
+ const allMatchingDeps = new Set<string>()
356
+
357
+ for (const packageJsonPath of packageJsonFiles) {
358
+ const deps = extractDependencies(packageJsonPath)
359
+ const matchingDeps: string[] = []
360
+
361
+ for (const dep of deps) {
362
+ // skip workspace packages
363
+ if (workspacePackageNames.has(dep)) continue
364
+
365
+ for (const pattern of packagePatterns) {
366
+ if (doesPackageMatchPattern(dep, pattern)) {
367
+ matchingDeps.push(dep)
368
+ allMatchingDeps.add(dep)
369
+ break
370
+ }
371
+ }
372
+ }
373
+
374
+ if (matchingDeps.length > 0) {
375
+ const dir = packageJsonPath.replace('/package.json', '')
376
+ const name = getWorkspaceName(packageJsonPath, rootDir)
377
+ packagesByWorkspace.set(name, { dir, packages: matchingDeps })
378
+ }
379
+ }
380
+
381
+ if (allMatchingDeps.size === 0) {
382
+ // no existing deps matched, but exact patterns (no wildcards) can be added fresh
383
+ const exactPatterns = packagePatterns.filter((p) => !p.includes('*'))
384
+ if (exactPatterns.length === 0) {
385
+ console.info(
386
+ `Found 0 dependencies matching patterns: ${packagePatterns.join(', ')}`
387
+ )
388
+ console.info('No matching packages found to update.')
389
+ return
390
+ }
391
+
392
+ // add as new dependencies to the root package.json
393
+ console.info(`No existing deps found, adding to root: ${exactPatterns.join(', ')}`)
394
+ const rootPkgPath = path.join(rootDir, 'package.json')
395
+
396
+ for (const pkg of exactPatterns) {
397
+ allMatchingDeps.add(pkg)
398
+ }
399
+
400
+ packagesByWorkspace.set('root', {
401
+ dir: rootDir,
402
+ packages: exactPatterns,
403
+ })
404
+
405
+ // insert placeholder so updatePackageJsonVersions can set the real version
406
+ const rootPkg = JSON.parse(fs.readFileSync(rootPkgPath, 'utf-8'))
407
+ if (!rootPkg.dependencies) {
408
+ rootPkg.dependencies = {}
409
+ }
410
+ for (const pkg of exactPatterns) {
411
+ if (!rootPkg.dependencies[pkg] && !rootPkg.devDependencies?.[pkg]) {
412
+ rootPkg.dependencies[pkg] = '*'
413
+ }
414
+ }
415
+ fs.writeFileSync(rootPkgPath, JSON.stringify(rootPkg, null, 2) + '\n')
416
+ } else {
417
+ console.info(
418
+ `Found ${allMatchingDeps.size} dependencies matching patterns: ${packagePatterns.join(', ')}`
419
+ )
420
+ console.info(`Found matches in ${packagesByWorkspace.size} workspace(s)`)
421
+ }
422
+
423
+ if (globalTag) {
424
+ console.info(`šŸ·ļø Using tag '${globalTag}'`)
425
+ }
426
+
427
+ await updatePackages(packagesByWorkspace, rootDir, packageJsonFiles)
428
+
429
+ // sync resolved $dep: values (like ZERO_VERSION) to all env targets
430
+ if (
431
+ Object.values(rootPackageJson.env || {}).some(
432
+ (v: any) => typeof v === 'string' && v.startsWith('$dep:')
433
+ )
434
+ ) {
435
+ console.info('\nšŸ”„ Syncing env variables...')
436
+ await $`bun tko run env-update`
437
+ }
438
+
439
+ console.info('\nšŸŽ‰ Dependency update complete!')
440
+ })
@@ -0,0 +1,92 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import fs from 'node:fs'
4
+
5
+ import { cmd } from './cmd'
6
+
7
+ await cmd`update changelog with recent git commits`.run(async ({ path }) => {
8
+ const { execSync } = await import('node:child_process')
9
+
10
+ const CHANGELOG_PATH = path.join(process.cwd(), 'src/features/site/docs/changelog.mdx')
11
+
12
+ function getLastSha(): string | null {
13
+ if (!fs.existsSync(CHANGELOG_PATH)) return null
14
+
15
+ try {
16
+ const content = fs.readFileSync(CHANGELOG_PATH, 'utf-8')
17
+ const match = content.match(/\{\/\* last updated: ([a-f0-9]+) \*\/\}/)
18
+ return match?.[1] || null
19
+ } catch {
20
+ return null
21
+ }
22
+ }
23
+
24
+ function getLatestSha(): string {
25
+ return execSync('git rev-parse --short HEAD', { encoding: 'utf-8' }).trim()
26
+ }
27
+
28
+ function getCommitCount(fromSha: string, toSha: string): number {
29
+ try {
30
+ const result = execSync(`git rev-list --count ${fromSha}..${toSha} --no-merges`, {
31
+ encoding: 'utf-8',
32
+ })
33
+ return parseInt(result.trim(), 10)
34
+ } catch {
35
+ return 0
36
+ }
37
+ }
38
+
39
+ const lastSha = getLastSha()
40
+ const latestSha = getLatestSha()
41
+
42
+ if (!lastSha) {
43
+ console.info(`no last sha found in changelog, defaulting to 4 weeks ago`)
44
+ }
45
+
46
+ const fromRef = lastSha || '$(git log --since="4 weeks ago" --format="%H" | tail -1)'
47
+ const commitCount = lastSha ? getCommitCount(lastSha, latestSha) : '~30'
48
+
49
+ if (commitCount === 0) {
50
+ console.info('no new commits since last update')
51
+ process.exit(0)
52
+ }
53
+
54
+ console.info(`
55
+ Update the changelog at: ${CHANGELOG_PATH}
56
+
57
+ Commit range: ${fromRef}..${latestSha} (${commitCount} commits)
58
+
59
+ Instructions:
60
+ - investigate commits with git log, git show, git diff as needed
61
+ - for vague commits like "cleanups" or "fix Component.tsx", look at the actual diff to understand what changed
62
+ - update the "last updated" comment to: ${latestSha}
63
+ - create new week sections if needed (weeks start monday)
64
+ - format: "## Week of January 20, 2026" with sha in backticks below
65
+ - no emoji, plain text only
66
+ - group small fixes/chores into "Bug fixes and chores (N)"
67
+ - for tamagui/one upgrades, check ~/tamagui or ~/one repos or github for what actually changed
68
+ - write useful descriptions - explain WHY and WHAT improved, not just "fixed X"
69
+ - stop when you reach commits already in the changelog
70
+
71
+ Archiving old entries:
72
+ - keep only 10 weeks in changelog.mdx
73
+ - when over 10 weeks, move the oldest entries into an archive file named
74
+ changelog-YYYY.mdx, where YYYY is the year the archived WEEK is from
75
+ (not the current year). e.g. a "Week of January 27, 2026" entry belongs
76
+ in changelog-2026.mdx, not changelog-2025.mdx — even if 2025 already
77
+ exists as an archive. never mix years in the same archive file.
78
+ - if the target archive file doesn't exist yet, create it with frontmatter:
79
+ ---
80
+ title: Changelog Archive — YYYY
81
+ description: Archived Takeout updates from YYYY
82
+ ---
83
+ and end the file with a link chain to the previous year's archive if
84
+ one exists, e.g. "See [2025 changes](/docs/changelog-2025) for earlier updates."
85
+ - if the archive already exists, prepend the new weeks to the top
86
+ (after frontmatter), so it stays in reverse-chronological order
87
+ - at the bottom of changelog.mdx, list links to every archive file that
88
+ exists, newest first, e.g.
89
+ "See [early 2026 changes](/docs/changelog-2026) and [2025 changes](/docs/changelog-2025) for previous updates."
90
+ (use absolute paths, update the list whenever a new archive file is created)
91
+ `)
92
+ })
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bun
2
+
3
+ import { cmd } from './cmd'
4
+
5
+ await cmd`wait for dev server to be available`.run(async () => {
6
+ const { sleep } = await import('@o/helpers')
7
+
8
+ const ONE_SERVER_URL = process.env.ONE_SERVER_URL || 'http://localhost:8081'
9
+ const CHECK_INTERVAL = 2000
10
+
11
+ async function checkServer(): Promise<boolean> {
12
+ try {
13
+ await fetch(ONE_SERVER_URL, {
14
+ signal: AbortSignal.timeout(5000),
15
+ }).then((res) => res.text())
16
+ // give it a couple seconds to build initial route
17
+ await sleep(2000)
18
+ return true
19
+ } catch (error) {
20
+ return false
21
+ }
22
+ }
23
+
24
+ process.stdout.write(`Waiting for server at ${ONE_SERVER_URL}...\n`)
25
+
26
+ while (true) {
27
+ await new Promise((resolve) => setTimeout(resolve, CHECK_INTERVAL))
28
+ const isReady = await checkServer()
29
+
30
+ if (isReady) {
31
+ process.stdout.write(`āœ“ Server at ${ONE_SERVER_URL} is ready!\n`)
32
+ break
33
+ }
34
+
35
+ process.stdout.write(
36
+ `Waiting to start dev watch until after dev server (wait ${CHECK_INTERVAL / 1000}s)...\n`
37
+ )
38
+ }
39
+
40
+ process.exit(0)
41
+ })