@take-out/scripts 0.0.52 → 0.0.58

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,17 +1,21 @@
1
1
  {
2
2
  "name": "@take-out/scripts",
3
- "version": "0.0.52",
3
+ "version": "0.0.58",
4
4
  "type": "module",
5
5
  "main": "./src/run.ts",
6
6
  "sideEffects": false,
7
7
  "exports": {
8
+ ".": {
9
+ "types": "./types/run.d.ts",
10
+ "default": "./src/run.ts"
11
+ },
8
12
  "./package.json": "./package.json",
9
13
  "./helpers/*": {
10
- "types": "./src/helpers/*.ts",
14
+ "types": "./types/helpers/*.d.ts",
11
15
  "default": "./src/helpers/*.ts"
12
16
  },
13
17
  "./*": {
14
- "types": "./src/*.ts",
18
+ "types": "./types/*.d.ts",
15
19
  "default": "./src/*.ts"
16
20
  }
17
21
  },
@@ -24,10 +28,9 @@
24
28
  "access": "public"
25
29
  },
26
30
  "dependencies": {
27
- "@take-out/helpers": "0.0.52",
28
- "glob": "^11.0.0"
31
+ "@take-out/helpers": "0.0.58"
29
32
  },
30
33
  "devDependencies": {
31
- "vxrn": "*"
34
+ "vxrn": "*1.4.0"
32
35
  }
33
36
  }
@@ -6,20 +6,12 @@
6
6
  */
7
7
 
8
8
  import { execSync } from 'node:child_process'
9
- import { parseArgs } from 'node:util'
10
9
 
11
- const { values, positionals } = parseArgs({
12
- args: process.argv.slice(2),
13
- options: {
14
- 'auto-kill': {
15
- type: 'string',
16
- short: 'k',
17
- },
18
- },
19
- allowPositionals: true,
20
- })
10
+ import { args } from './helpers/args'
21
11
 
22
- const port = positionals[0]
12
+ const { autoKill, _ } = args('--auto-kill string')
13
+
14
+ const port = _[0]
23
15
 
24
16
  if (!port) {
25
17
  console.error('usage: bun tko ensure-port <port> [--auto-kill <prefix>]')
@@ -71,10 +63,8 @@ function killProcess(pid: number): boolean {
71
63
  const { pid, command } = getListeningProcess(portNum)
72
64
 
73
65
  if (pid) {
74
- const autoKillPrefix = values['auto-kill']
75
-
76
66
  // check if we should auto-kill this process
77
- if (autoKillPrefix && command?.startsWith(autoKillPrefix)) {
67
+ if (autoKill && command?.startsWith(autoKill)) {
78
68
  console.info(`killing ${command} (pid ${pid}) on port ${portNum}`)
79
69
  if (killProcess(pid)) {
80
70
  // give it a moment to release the port
@@ -0,0 +1,95 @@
1
+ /**
2
+ * ultra simple typed arg parsing
3
+ *
4
+ * @example
5
+ * const { port, verbose, _ } = args`--port number --verbose boolean`
6
+ * // port: number | undefined
7
+ * // verbose: boolean
8
+ * // _: string[]
9
+ */
10
+
11
+ type ParseType<T extends string> = T extends 'number'
12
+ ? number | undefined
13
+ : T extends 'boolean'
14
+ ? boolean
15
+ : T extends 'string'
16
+ ? string | undefined
17
+ : never
18
+
19
+ type ParseFlag<S extends string> = S extends `--${infer Name} ${infer Type}`
20
+ ? { [K in Name as KebabToCamel<K>]: ParseType<Type> }
21
+ : S extends `-${infer Name} ${infer Type}`
22
+ ? { [K in Name as KebabToCamel<K>]: ParseType<Type> }
23
+ : object
24
+
25
+ type KebabToCamel<S extends string> = S extends `${infer A}-${infer B}`
26
+ ? `${A}${Capitalize<KebabToCamel<B>>}`
27
+ : S
28
+
29
+ // trim leading/trailing whitespace from string type
30
+ type Trim<S extends string> = S extends ` ${infer R}`
31
+ ? Trim<R>
32
+ : S extends `${infer R} `
33
+ ? Trim<R>
34
+ : S extends `\n${infer R}`
35
+ ? Trim<R>
36
+ : S extends `${infer R}\n`
37
+ ? Trim<R>
38
+ : S
39
+
40
+ // split on -- allowing any whitespace between flags
41
+ type ParseFlags<S extends string> =
42
+ Trim<S> extends `${infer Flag} --${infer Rest}`
43
+ ? ParseFlag<Trim<Flag>> & ParseFlags<`--${Rest}`>
44
+ : Trim<S> extends `${infer Flag}\n--${infer Rest}`
45
+ ? ParseFlag<Trim<Flag>> & ParseFlags<`--${Rest}`>
46
+ : Trim<S> extends `${infer Flag} -${infer Rest}`
47
+ ? ParseFlag<Trim<Flag>> & ParseFlags<`-${Rest}`>
48
+ : Trim<S> extends `${infer Flag}\n-${infer Rest}`
49
+ ? ParseFlag<Trim<Flag>> & ParseFlags<`-${Rest}`>
50
+ : ParseFlag<Trim<S>>
51
+
52
+ // flatten intersection into single object for nice hover display
53
+ type Prettify<T> = { [K in keyof T]: T[K] } & {}
54
+
55
+ type Args<S extends string> = Prettify<ParseFlags<S> & { _: string[] }>
56
+
57
+ export function args<const S extends string>(spec: S): Args<S>
58
+ export function args<const S extends string>(strings: TemplateStringsArray | S): Args<S> {
59
+ const spec = typeof strings === 'string' ? strings : strings[0] || ''
60
+ const argv = process.argv.slice(2)
61
+
62
+ // parse spec: "--port number --verbose boolean"
63
+ const schema: Record<string, 'string' | 'number' | 'boolean'> = {}
64
+ const matches = spec.matchAll(/--?([a-z-]+)\s+(string|number|boolean)/gi)
65
+
66
+ for (const [, flag, type] of matches) {
67
+ const camel = flag!.replace(/-([a-z])/g, (_, c) => c.toUpperCase())
68
+ schema[camel] = type as 'string' | 'number' | 'boolean'
69
+ }
70
+
71
+ const result: Record<string, string | number | boolean | undefined> = {}
72
+ const rest: string[] = []
73
+
74
+ for (let i = 0; i < argv.length; i++) {
75
+ const arg = argv[i]!
76
+
77
+ if (arg.startsWith('-')) {
78
+ const key = arg.replace(/^--?/, '').replace(/-([a-z])/g, (_, c) => c.toUpperCase())
79
+ const type = schema[key]
80
+
81
+ if (type === 'boolean') {
82
+ result[key] = true
83
+ } else if (type === 'string' || type === 'number') {
84
+ const val = argv[++i]
85
+ result[key] = type === 'number' ? Number(val) : val
86
+ } else {
87
+ rest.push(arg)
88
+ }
89
+ } else {
90
+ rest.push(arg)
91
+ }
92
+ }
93
+
94
+ return { ...result, _: rest } as Args<S>
95
+ }
@@ -1,3 +1,4 @@
1
+ import { readFileSync } from 'node:fs'
1
2
  import { join } from 'node:path'
2
3
 
3
4
  import { loadEnv } from './env-load'
@@ -16,23 +17,44 @@ function getPortFromConnectionString(url: string | undefined): string | undefine
16
17
  }
17
18
  }
18
19
 
20
+ // read a specific env var directly from .env.development file
21
+ // (process.env may have production values if NODE_ENV=production)
22
+ function getDevEnvVar(key: string): string | undefined {
23
+ try {
24
+ const envPath = join(process.cwd(), '.env.development')
25
+ const content = readFileSync(envPath, 'utf-8')
26
+ const regex = new RegExp(`^${key}=["']?([^"'\\n]+)["']?`, 'm')
27
+ const match = content.match(regex)
28
+ return match?.[1]
29
+ } catch {
30
+ return undefined
31
+ }
32
+ }
33
+
34
+ function getDevDbPort(): string | undefined {
35
+ const url = getDevEnvVar('ZERO_UPSTREAM_DB')
36
+ return url ? getPortFromConnectionString(url) : undefined
37
+ }
38
+
19
39
  export async function getTestEnv() {
20
40
  const zeroVersion = getZeroVersion()
21
- const devEnv = await loadEnv('development')
22
41
  const dockerHost = getDockerHost()
42
+ const devEnv = await loadEnv('development')
23
43
  const serverEnvFallback = await import(join(process.cwd(), 'src/server/env-server'))
24
44
 
25
45
  // determine db port from (in order of priority):
26
46
  // 1. DOCKER_DB_PORT env var
27
- // 2. port from ZERO_UPSTREAM_DB in devEnv
47
+ // 2. port from ZERO_UPSTREAM_DB in .env.development file (read directly to avoid
48
+ // bun auto-loading .env.production when NODE_ENV=production)
28
49
  // 3. default 5432
29
- const dbPort =
30
- process.env.DOCKER_DB_PORT ||
31
- getPortFromConnectionString(devEnv.ZERO_UPSTREAM_DB as string) ||
32
- '5432'
50
+ const dbPort = process.env.DOCKER_DB_PORT || getDevDbPort() || '5432'
33
51
 
34
52
  const dockerDbBase = `postgresql://user:password@127.0.0.1:${dbPort}`
35
53
 
54
+ // use dev BETTER_AUTH_SECRET for CI/staging to match local dev database keys
55
+ // (better-auth encrypts JWKS private keys with this secret, so it must match)
56
+ const devAuthSecret = getDevEnvVar('BETTER_AUTH_SECRET')
57
+
36
58
  return {
37
59
  ...serverEnvFallback,
38
60
  ...devEnv,
@@ -51,5 +73,7 @@ export async function getTestEnv() {
51
73
  CLOUDFLARE_R2_PUBLIC_URL: 'http://127.0.0.1:9200',
52
74
  CLOUDFLARE_R2_ACCESS_KEY: 'minio',
53
75
  CLOUDFLARE_R2_SECRET_KEY: 'minio_password',
76
+ // ensure auth secret matches dev db keys
77
+ ...(devAuthSecret && { BETTER_AUTH_SECRET: devAuthSecret }),
54
78
  }
55
79
  }
@@ -84,7 +84,9 @@ export async function run(
84
84
  return runInternal()
85
85
 
86
86
  async function runInternal() {
87
- if (!silent) {
87
+ // respect TKO_SILENT env var for quiet mode in watch scenarios
88
+ const effectiveSilent = silent || process.env.TKO_SILENT === '1'
89
+ if (!effectiveSilent) {
88
90
  console.info(`$ ${command}${cwd ? ` (in ${cwd})` : ``}`)
89
91
  }
90
92
 
@@ -117,7 +119,7 @@ export async function run(
117
119
  const coloredPrefix = prefix ? `${color}[${prefix}]${reset}` : ''
118
120
 
119
121
  const writeOutput = (text: string, isStderr: boolean) => {
120
- if (!silent) {
122
+ if (!effectiveSilent) {
121
123
  const output = prefix ? `${coloredPrefix} ${text}` : text
122
124
  if (!prefix || !captureOutput) {
123
125
  const stream = isStderr ? process.stderr : process.stdout
@@ -130,7 +132,7 @@ export async function run(
130
132
  stream: ReadableStream<Uint8Array> | undefined,
131
133
  isStderr: boolean
132
134
  ): Promise<string> => {
133
- if (silent && !captureOutput) {
135
+ if (effectiveSilent && !captureOutput) {
134
136
  return ''
135
137
  }
136
138
 
package/src/release.ts CHANGED
@@ -127,7 +127,8 @@ async function main() {
127
127
 
128
128
  if (!skipTest) {
129
129
  await run(`bun lint`)
130
- await run(`bun check`)
130
+ await run(`bun check:all`)
131
+ // only in packages
131
132
  // await run(`bun test`)
132
133
  }
133
134
  }
@@ -180,6 +181,8 @@ async function main() {
180
181
 
181
182
  if (!finish) {
182
183
  const tmpDir = `/tmp/one-publish`
184
+ // clean up from previous runs
185
+ await fs.remove(tmpDir)
183
186
  await ensureDir(tmpDir)
184
187
 
185
188
  await pMap(
@@ -216,6 +219,27 @@ async function main() {
216
219
 
217
220
  const filename = `${name.replace('/', '_')}-package.tmp.tgz`
218
221
  const absolutePath = `${tmpDir}/${filename}`
222
+
223
+ // swap exports.types from ./src/*.ts to ./types/*.d.ts for publishing
224
+ if (pkgJson.exports) {
225
+ const swapTypes = (obj: any) => {
226
+ for (const key in obj) {
227
+ const val = obj[key]
228
+ if (typeof val === 'object' && val !== null) {
229
+ swapTypes(val)
230
+ } else if (
231
+ key === 'types' &&
232
+ typeof val === 'string' &&
233
+ val.includes('/src/')
234
+ ) {
235
+ obj[key] = val.replace('/src/', '/types/').replace('.ts', '.d.ts')
236
+ }
237
+ }
238
+ }
239
+ swapTypes(pkgJson.exports)
240
+ await writeJSON(pkgJsonPath, pkgJson, { spaces: 2 })
241
+ }
242
+
219
243
  await run(`npm pack --pack-destination ${tmpDir}`, {
220
244
  cwd: tmpPackageDir,
221
245
  silent: true,
package/src/run.ts CHANGED
@@ -13,18 +13,22 @@ import { handleProcessExit } from '@take-out/scripts/helpers/handleProcessExit'
13
13
  import { getIsExiting } from './helpers/run'
14
14
  import { checkNodeVersion } from './node-version-check'
15
15
 
16
+ // 256-color grays for subtle differentiation (232=darkest, 255=lightest)
16
17
  const colors = [
17
- '\x1b[36m', // Cyan
18
- '\x1b[35m', // Magenta
19
- '\x1b[33m', // Yellow
20
- '\x1b[32m', // Green
21
- '\x1b[34m', // Blue
22
- '\x1b[31m', // Red
23
- '\x1b[90m', // Gray
18
+ '\x1b[38;5;245m', // medium gray
19
+ '\x1b[38;5;240m', // darker gray
20
+ '\x1b[38;5;250m', // lighter gray
21
+ '\x1b[38;5;243m', // medium-dark gray
22
+ '\x1b[38;5;248m', // medium-light gray
23
+ '\x1b[38;5;238m', // dark gray
24
+ '\x1b[38;5;252m', // light gray
24
25
  ]
25
26
 
26
27
  const reset = '\x1b[0m'
27
28
 
29
+ // eslint-disable-next-line no-control-regex
30
+ const ansiPattern = /\x1b\[[0-9;]*m/g
31
+
28
32
  // Verbose logging flag - set to false to reduce logs
29
33
  const verbose = false
30
34
 
@@ -40,11 +44,38 @@ const log = {
40
44
  const MAX_RESTARTS = 3
41
45
 
42
46
  // Separate command names from flags/arguments
47
+ // Handles --flag=value and --flag value styles, excluding flag values from commands
43
48
  const args = process.argv.slice(2)
44
- const runCommands = args.filter((x) => !x.startsWith('--'))
49
+ const ownFlags = ['--no-root', '--bun', '--watch', '--flags=last']
50
+ const runCommands: string[] = []
51
+ const forwardArgs: string[] = []
52
+
53
+ for (let i = 0; i < args.length; i++) {
54
+ const arg = args[i]!
55
+
56
+ if (arg.startsWith('--')) {
57
+ // handle flags
58
+ if (ownFlags.includes(arg) || arg.startsWith('--stdin=')) {
59
+ continue
60
+ }
61
+ forwardArgs.push(arg)
62
+ // if next arg exists and doesn't start with --, treat it as this flag's value
63
+ const nextArg = args[i + 1]
64
+ if (nextArg && !nextArg.startsWith('--')) {
65
+ forwardArgs.push(nextArg)
66
+ i++ // skip the value in next iteration
67
+ }
68
+ } else {
69
+ // non-flag arg is a command name
70
+ runCommands.push(arg)
71
+ }
72
+ }
73
+
45
74
  const noRoot = args.includes('--no-root')
46
75
  const runBun = args.includes('--bun')
47
76
  const watch = args.includes('--watch') // just attempts to restart a failed process up to MAX_RESTARTS times
77
+ // --flags=last forwards args only to last script, default forwards to all
78
+ const flagsLast = args.includes('--flags=last')
48
79
 
49
80
  // parse --stdin=<script-name> to specify which script receives keyboard input
50
81
  // if not specified, defaults to the last script in the list
@@ -53,16 +84,6 @@ const stdinScript = stdinArg
53
84
  ? stdinArg.replace('--stdin=', '')
54
85
  : (runCommands[runCommands.length - 1] ?? null)
55
86
 
56
- // Collect additional flags and arguments to forward to sub-commands
57
- const forwardArgs = args.filter(
58
- (arg) =>
59
- arg.startsWith('--') &&
60
- arg !== '--no-root' &&
61
- arg !== '--bun' &&
62
- arg !== '--watch' &&
63
- !arg.startsWith('--stdin=')
64
- )
65
-
66
87
  // Get the list of scripts already being run by a parent process
67
88
  const parentRunningScripts = process.env.BUN_RUN_SCRIPTS
68
89
  ? process.env.BUN_RUN_SCRIPTS.split(',')
@@ -217,7 +238,8 @@ const runScript = async (
217
238
  name: string,
218
239
  cwd = '.',
219
240
  prefixLabel: string = name,
220
- restarts = 0
241
+ restarts = 0,
242
+ extraArgs: string[] = []
221
243
  ) => {
222
244
  const colorIndex = processes.length % colors.length
223
245
  const color = colors[colorIndex]
@@ -226,7 +248,10 @@ const runScript = async (
226
248
  let stderrBuffer = ''
227
249
 
228
250
  // Construct command with arguments to forward
229
- const runArgs = ['run', runBun ? '--bun' : '', name, ...forwardArgs].filter(Boolean)
251
+ // --silent suppresses bun's "$ command" output
252
+ const runArgs = ['run', '--silent', runBun ? '--bun' : '', name, ...extraArgs].filter(
253
+ Boolean
254
+ )
230
255
 
231
256
  // Log the exact command being run
232
257
  const commandDisplay = `bun ${runArgs.join(' ')}`
@@ -247,6 +272,8 @@ const runScript = async (
247
272
  FORCE_COLOR: '3',
248
273
  BUN_RUN_PARENT_SCRIPT: name,
249
274
  BUN_RUN_SCRIPTS: allRunningScripts,
275
+ // propagate silent mode to child scripts
276
+ TKO_SILENT: '1',
250
277
  } as any,
251
278
  cwd: resolve(cwd),
252
279
  detached: true,
@@ -261,6 +288,9 @@ const runScript = async (
261
288
  if (getIsExiting()) return // prevent output during cleanup
262
289
  const lines = data.toString().split('\n')
263
290
  for (const line of lines) {
291
+ // filter out bun's "$ command" echo lines in nested scripts
292
+ const stripped = line.replace(ansiPattern, '')
293
+ if (stripped.startsWith('$ ')) continue
264
294
  if (line) log.output(`${color}${prefixLabel}${reset} ${line}`)
265
295
  }
266
296
  })
@@ -272,6 +302,9 @@ const runScript = async (
272
302
  if (getIsExiting()) return // prevent output during cleanup
273
303
  const lines = dataStr.split('\n')
274
304
  for (const line of lines) {
305
+ // filter out bun's "$ command" echo lines in nested scripts
306
+ const stripped = line.replace(ansiPattern, '')
307
+ if (stripped.startsWith('$ ')) continue
275
308
  if (line) log.error(`${color}${prefixLabel}${reset} ${line}`)
276
309
  }
277
310
  })
@@ -290,35 +323,17 @@ const runScript = async (
290
323
  log.error(`${color}${prefixLabel}${reset} Process exited with code ${code}`)
291
324
 
292
325
  if (code === 1) {
293
- // Print a nicer error message with red header
294
- console.error(
295
- '\n\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m'
296
- )
297
326
  console.error('\x1b[31māŒ Run Failed\x1b[0m')
298
- console.error(
299
- '\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m'
300
- )
301
327
  console.error(
302
328
  `\x1b[31mProcess "${prefixLabel}" failed with exit code ${code}\x1b[0m`
303
329
  )
304
330
 
305
- // Show the original error output without prefixes
306
- if (stderrBuffer.trim()) {
307
- console.error('\n\x1b[31mError output:\x1b[0m')
308
- console.error('\x1b[90m' + '─'.repeat(80) + '\x1b[0m')
309
- const cleanedStderr = stderrBuffer
310
- console.error(cleanedStderr)
311
- console.error('\x1b[90m' + '─'.repeat(80) + '\x1b[0m')
312
- }
313
-
314
- console.error('')
315
-
316
331
  if (watch && restarts < MAX_RESTARTS) {
317
332
  const newRestarts = restarts + 1
318
333
  console.info(
319
334
  `Restarting process ${name} (${newRestarts}/${MAX_RESTARTS} times)`
320
335
  )
321
- runScript(name, cwd, prefixLabel, newRestarts)
336
+ runScript(name, cwd, prefixLabel, newRestarts, extraArgs)
322
337
  } else {
323
338
  exit(1)
324
339
  }
@@ -337,11 +352,18 @@ async function main() {
337
352
 
338
353
  try {
339
354
  if (runCommands.length > 0) {
355
+ const lastScript = runCommands[runCommands.length - 1]
356
+
340
357
  // Root package.json scripts first, if not disabled
341
358
  if (!noRoot) {
342
- const scriptPromises = runCommands
343
- .filter((name) => !parentRunningScripts.includes(name))
344
- .map((name) => runScript(name))
359
+ const filteredCommands = runCommands.filter(
360
+ (name) => !parentRunningScripts.includes(name)
361
+ )
362
+ const scriptPromises = filteredCommands.map((name) => {
363
+ // --flags=last: only forward args to last script
364
+ const args = !flagsLast || name === lastScript ? forwardArgs : []
365
+ return runScript(name, '.', name, 0, args)
366
+ })
345
367
 
346
368
  await Promise.all(scriptPromises)
347
369
  }
@@ -349,11 +371,14 @@ async function main() {
349
371
  const workspaceScriptMap = await mapWorkspacesToScripts(runCommands)
350
372
 
351
373
  for (const [workspace, { scripts, packageName }] of workspaceScriptMap.entries()) {
352
- const workspaceScriptPromises = scripts
353
- .filter((scriptName) => !parentRunningScripts.includes(scriptName))
354
- .map((scriptName) =>
355
- runScript(scriptName, workspace, `[${packageName}] [${scriptName}]`)
356
- )
374
+ const filteredScripts = scripts.filter(
375
+ (scriptName) => !parentRunningScripts.includes(scriptName)
376
+ )
377
+ const workspaceScriptPromises = filteredScripts.map((scriptName) => {
378
+ // --flags=last: only forward args to last script
379
+ const args = !flagsLast || scriptName === lastScript ? forwardArgs : []
380
+ return runScript(scriptName, workspace, `${packageName} ${scriptName}`, 0, args)
381
+ })
357
382
 
358
383
  await Promise.all(workspaceScriptPromises)
359
384
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  /**
4
- * @description Update dependencies across workspace packages
4
+ * @description Upgrade packages by name (takeout, tamagui, one, zero, better-auth)
5
5
  */
6
6
 
7
7
  import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
@@ -14,7 +14,13 @@ const packagePatterns: string[] = []
14
14
 
15
15
  const args = process.argv.slice(2)
16
16
 
17
- for (const arg of args) {
17
+ // check if first arg is a named upgrade set
18
+ const rootDir = process.cwd()
19
+ const rootPackageJson = JSON.parse(readFileSync(join(rootDir, 'package.json'), 'utf-8'))
20
+ const upgradeSets: Record<string, string[]> = rootPackageJson.upgradeSets || {}
21
+
22
+ for (let i = 0; i < args.length; i++) {
23
+ const arg = args[i]
18
24
  if (arg.startsWith('--tag=')) {
19
25
  const tagValue = arg.split('=')[1]
20
26
  if (tagValue) {
@@ -26,21 +32,33 @@ for (const arg of args) {
26
32
  } else if (arg === '--tag') {
27
33
  console.error('Error: --tag option requires a value and must use = syntax.')
28
34
  console.error('Correct usage: --tag=canary')
29
- console.error('Example: bun update-deps.ts --tag=canary react react-dom')
35
+ console.error('Example: bun tko up --tag=canary react react-dom')
30
36
  process.exit(1)
31
37
  } else if (arg === '--canary') {
32
38
  globalTag = 'canary'
33
39
  } else if (arg === '--rc') {
34
40
  globalTag = 'rc'
41
+ } else if (arg in upgradeSets) {
42
+ // expand named upgrade set to its patterns
43
+ packagePatterns.push(...upgradeSets[arg])
35
44
  } else {
36
45
  packagePatterns.push(arg)
37
46
  }
38
47
  }
39
48
 
40
49
  if (packagePatterns.length === 0) {
41
- console.error('Please provide at least one package pattern to update.')
42
- console.error('Example: bun update-deps.ts @vxrn/* vxrn')
43
- console.error('Or with a tag: bun update-deps.ts --tag=canary @vxrn/* vxrn')
50
+ const setNames = Object.keys(upgradeSets)
51
+ if (setNames.length > 0) {
52
+ console.info('Usage: bun tko up <target|pattern> [options]')
53
+ console.info(`\nAvailable upgrade sets: ${setNames.join(', ')}`)
54
+ console.info('\nOr provide package patterns directly:')
55
+ console.info(' bun tko up @vxrn/* vxrn')
56
+ console.info(' bun tko up --tag=canary react react-dom')
57
+ } else {
58
+ console.error('Please provide at least one package pattern to update.')
59
+ console.error('Example: bun tko up @vxrn/* vxrn')
60
+ console.error('Or with a tag: bun tko up --tag=canary @vxrn/* vxrn')
61
+ }
44
62
  process.exit(1)
45
63
  }
46
64
 
@@ -58,7 +76,7 @@ function findPackageJsonFiles(dir: string): string[] {
58
76
  results.push(join(dir, 'package.json'))
59
77
  }
60
78
 
61
- // Check if it's a monorepo with workspaces
79
+ // check if it's a monorepo with workspaces
62
80
  try {
63
81
  const packageJson = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf-8'))
64
82
  if (packageJson.workspaces) {
@@ -71,22 +89,60 @@ function findPackageJsonFiles(dir: string): string[] {
71
89
  }
72
90
 
73
91
  for (const workspace of workspacePaths) {
74
- const workspaceDir = workspace.replace(/\/\*$/, '')
75
- if (existsSync(join(dir, workspaceDir))) {
76
- const subdirs = readdirSync(join(dir, workspaceDir), { withFileTypes: true })
77
- .filter((dirent) => dirent.isDirectory())
78
- .map((dirent) => join(dir, workspaceDir, dirent.name))
79
-
80
- for (const subdir of subdirs) {
81
- if (existsSync(join(subdir, 'package.json'))) {
82
- results.push(join(subdir, 'package.json'))
92
+ // handle glob patterns like "packages/*", "code/**/*", "./code/ui/**/*"
93
+ const normalizedWorkspace = workspace.replace(/^\.\//, '')
94
+
95
+ if (normalizedWorkspace.includes('**')) {
96
+ // nested glob pattern - use glob to find all package.json files
97
+ const baseDir = normalizedWorkspace.split('**')[0].replace(/\/$/, '')
98
+ const basePath = join(dir, baseDir)
99
+
100
+ if (existsSync(basePath)) {
101
+ const findPackages = (searchDir: string) => {
102
+ try {
103
+ const entries = readdirSync(searchDir, { withFileTypes: true })
104
+ for (const entry of entries) {
105
+ if (entry.isDirectory()) {
106
+ const subPath = join(searchDir, entry.name)
107
+ const pkgPath = join(subPath, 'package.json')
108
+ if (existsSync(pkgPath)) {
109
+ results.push(pkgPath)
110
+ }
111
+ // recurse into subdirectories
112
+ findPackages(subPath)
113
+ }
114
+ }
115
+ } catch (_e) {
116
+ // ignore permission errors
117
+ }
118
+ }
119
+ findPackages(basePath)
120
+ }
121
+ } else if (normalizedWorkspace.includes('*')) {
122
+ // simple glob pattern like "packages/*"
123
+ const workspaceDir = normalizedWorkspace.replace(/\/\*$/, '')
124
+ if (existsSync(join(dir, workspaceDir))) {
125
+ const subdirs = readdirSync(join(dir, workspaceDir), { withFileTypes: true })
126
+ .filter((dirent) => dirent.isDirectory())
127
+ .map((dirent) => join(dir, workspaceDir, dirent.name))
128
+
129
+ for (const subdir of subdirs) {
130
+ if (existsSync(join(subdir, 'package.json'))) {
131
+ results.push(join(subdir, 'package.json'))
132
+ }
83
133
  }
84
134
  }
135
+ } else {
136
+ // exact path like "code/tamagui.dev" or "./code/sandbox"
137
+ const pkgPath = join(dir, normalizedWorkspace, 'package.json')
138
+ if (existsSync(pkgPath)) {
139
+ results.push(pkgPath)
140
+ }
85
141
  }
86
142
  }
87
143
  }
88
144
  } catch (_error) {
89
- // Ignore errors parsing package.json
145
+ // ignore errors parsing package.json
90
146
  }
91
147
 
92
148
  return results
@@ -258,7 +314,6 @@ function getWorkspaceName(packageJsonPath: string, rootDir: string): string {
258
314
  }
259
315
 
260
316
  async function main() {
261
- const rootDir = process.cwd()
262
317
  const packageJsonFiles = findPackageJsonFiles(rootDir)
263
318
  console.info(`Found ${packageJsonFiles.length} package.json files`)
264
319
 
@@ -326,6 +381,12 @@ async function main() {
326
381
 
327
382
  await updatePackages(packagesByWorkspace, rootDir, packageJsonFiles)
328
383
 
384
+ // special handling for zero - update ZERO_VERSION in .env
385
+ if (packagePatterns.includes('@rocicorp/zero')) {
386
+ console.info('\nšŸ”„ Updating local env for Zero...')
387
+ await $`bun tko run update-local-env`
388
+ }
389
+
329
390
  console.info('\nšŸŽ‰ Dependency update complete!')
330
391
  }
331
392
 
@@ -1,129 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- import { basename } from 'node:path'
4
-
5
- import { $ } from 'bun'
6
- import { globSync } from 'glob'
7
-
8
- const projectRoot = process.cwd()
9
-
10
- // parse --ignore flag
11
- const args = process.argv.slice(2)
12
- const ignoreIndex = args.indexOf('--ignore')
13
- const ignoredFiles: string[] = []
14
-
15
- if (ignoreIndex !== -1 && args[ignoreIndex + 1]) {
16
- // split comma-separated files
17
- const ignoreArg = args[ignoreIndex + 1]
18
- if (ignoreArg) {
19
- const ignoreList = ignoreArg.split(',')
20
- ignoredFiles.push(...ignoreList.map((f) => f.trim()))
21
- }
22
- }
23
-
24
- // glob all tsx files in app directory
25
- const appFiles = globSync('app/**/*.tsx', {
26
- cwd: projectRoot,
27
- absolute: true,
28
- })
29
-
30
- // glob all tsx files in src directory
31
- const srcFiles = globSync('src/**/*.tsx', {
32
- cwd: projectRoot,
33
- absolute: true,
34
- })
35
-
36
- // combine all files
37
- const allFiles = [...appFiles, ...srcFiles]
38
-
39
- if (allFiles.length === 0) {
40
- console.error('No tsx files found')
41
- process.exit(1)
42
- }
43
-
44
- // build the command with all -r flags
45
- const commandArgs = [
46
- 'bunx',
47
- '@glideapps/ts-helper',
48
- '-c', // check circular dependencies
49
- '-p',
50
- projectRoot,
51
- ...allFiles.flatMap((file) => ['-r', file]),
52
- ]
53
-
54
- console.info(`Checking circular dependencies for ${allFiles.length} files...`)
55
-
56
- try {
57
- // run the command and capture output
58
- await $`${commandArgs}`.quiet()
59
- console.info('āœ… No circular dependencies found')
60
- } catch (error: any) {
61
- // parse the output to check for cycles
62
- const output = error.stderr?.toString() || error.stdout?.toString() || ''
63
-
64
- // check if this is a tool crash (assertion error) rather than actual cycle detection
65
- if (output.includes('Assertion failed') || output.includes('at panic')) {
66
- const nodeVersion = process.versions.node
67
- const majorVersion = parseInt(nodeVersion.split('.')[0] || '0', 10)
68
- if (majorVersion >= 24) {
69
- console.warn(`āš ļø @glideapps/ts-helper crashes on Node.js ${nodeVersion}`)
70
- console.warn(' skipping locally - CI uses Node 20 where this check runs properly')
71
- process.exit(0)
72
- }
73
- // unknown crash on older node, fail
74
- console.error(output)
75
- console.error('āŒ Circular dependency check crashed unexpectedly')
76
- process.exit(1)
77
- }
78
-
79
- // parse cycle arrays from output
80
- const cycleRegex = /\[([^\]]+)\]/g
81
- const cycles: string[][] = []
82
- let match
83
-
84
- while ((match = cycleRegex.exec(output)) !== null) {
85
- // parse the cycle array
86
- const cycleStr = match[1]
87
- if (cycleStr) {
88
- const files = cycleStr.split(',').map((f) => f.trim().replace(/"/g, ''))
89
- cycles.push(files)
90
- }
91
- }
92
-
93
- // filter out cycles that contain ignored files
94
- const remainingCycles = cycles.filter((cycle) => {
95
- const containsIgnored = cycle.some((file) =>
96
- ignoredFiles.some((ignored) => basename(file) === ignored || file.includes(ignored))
97
- )
98
- return !containsIgnored
99
- })
100
-
101
- if (remainingCycles.length > 0) {
102
- // reconstruct output with filtered cycles
103
- console.info(
104
- output
105
- .split('\n')
106
- .filter((line: string) => !line.startsWith('['))
107
- .join('\n')
108
- )
109
- console.info(`Found ${remainingCycles.length} dependency cycles`)
110
- remainingCycles.forEach((cycle) => {
111
- console.info(JSON.stringify(cycle))
112
- })
113
- console.error('āŒ Circular dependencies detected')
114
- process.exit(1)
115
- } else if (cycles.length > 0) {
116
- // all cycles were ignored
117
- console.info(
118
- `āœ… Found ${cycles.length} circular dependencies but all contain ignored files`
119
- )
120
- if (ignoredFiles.length > 0) {
121
- console.info(` Ignored files: ${ignoredFiles.join(', ')}`)
122
- }
123
- } else {
124
- // no cycles found, but command failed for another reason
125
- console.error(output)
126
- console.error('āŒ Error running circular dependency check')
127
- process.exit(1)
128
- }
129
- }