@o/cli 0.16.19 → 0.16.20

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.
@@ -0,0 +1,83 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { existsSync, readFileSync } from 'node:fs'
3
+ import { resolve, parse } from 'node:path'
4
+
5
+ import { defineCommand } from 'citty'
6
+
7
+ function resolveNodeBinary(): string {
8
+ const explicitNode = process.env.NODE
9
+ if (explicitNode && existsSync(explicitNode)) {
10
+ return explicitNode
11
+ }
12
+
13
+ if (process.execPath.endsWith('/node') && existsSync(process.execPath)) {
14
+ return process.execPath
15
+ }
16
+
17
+ return 'node'
18
+ }
19
+
20
+ export const runAllCommand = defineCommand({
21
+ meta: {
22
+ name: 'run-all',
23
+ description: 'Run multiple package.json scripts in parallel',
24
+ },
25
+ args: {
26
+ scripts: {
27
+ type: 'positional',
28
+ description: 'Scripts to run in parallel',
29
+ required: false,
30
+ },
31
+ },
32
+ run: async () => {
33
+ const scriptArgs = process.argv.slice(3)
34
+ const usePty = scriptArgs.includes('--pty')
35
+ const filteredArgs = scriptArgs.filter((a) => a !== '--pty')
36
+
37
+ if (usePty) {
38
+ // pty mode still needs node + node-pty subprocess
39
+ const projectRoot = findProjectRoot()
40
+ if (!projectRoot) {
41
+ console.error('Could not find project root')
42
+ process.exit(1)
43
+ }
44
+
45
+ const localRunPath = resolve(projectRoot, 'packages/run/src/run-pty.mjs')
46
+ const scriptPath = existsSync(localRunPath)
47
+ ? localRunPath
48
+ : resolve(projectRoot, 'node_modules/@o/run/src/run-pty.mjs')
49
+
50
+ const child = spawn(resolveNodeBinary(), [scriptPath, ...filteredArgs], {
51
+ stdio: 'inherit',
52
+ shell: false,
53
+ env: process.env,
54
+ })
55
+
56
+ const code = await new Promise<number>((resolve) => {
57
+ child.on('exit', (code) => resolve(code || 0))
58
+ })
59
+ process.exit(code)
60
+ } else {
61
+ // direct import — no subprocess layer
62
+ const { runParallelScripts, parseRunArgs } = await import('@o/run')
63
+ await runParallelScripts(parseRunArgs(filteredArgs))
64
+ }
65
+ },
66
+ })
67
+
68
+ function findProjectRoot(): string {
69
+ let currentDir = process.cwd()
70
+ while (currentDir !== parse(currentDir).root) {
71
+ const packageJsonPath = resolve(currentDir, 'package.json')
72
+ if (existsSync(packageJsonPath)) {
73
+ try {
74
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf8'))
75
+ if (pkg.workspaces || pkg.takeout) {
76
+ return currentDir
77
+ }
78
+ } catch {}
79
+ }
80
+ currentDir = resolve(currentDir, '..')
81
+ }
82
+ return ''
83
+ }
@@ -0,0 +1,135 @@
1
+ import { spawn } from 'node:child_process'
2
+ import { existsSync, statSync } from 'node:fs'
3
+ import { join } from 'node:path'
4
+
5
+ import { defineCommand } from 'citty'
6
+
7
+ export const runCommand = defineCommand({
8
+ meta: {
9
+ name: 'run',
10
+ description: 'Run scripts in parallel (shorthand for "takeout script run")',
11
+ },
12
+ args: {
13
+ scripts: {
14
+ type: 'positional',
15
+ description: 'Scripts to run',
16
+ required: false,
17
+ },
18
+ },
19
+ run: async () => {
20
+ // Get all arguments after 'run'
21
+ const scriptArgs = process.argv.slice(3)
22
+ const firstArg = scriptArgs[0]
23
+ const secondArg = scriptArgs[1]
24
+
25
+ // Check if this is a filesystem script (category or script with slash)
26
+ if (firstArg) {
27
+ const localDir = join(process.cwd(), 'scripts')
28
+ const categoryPath = join(localDir, firstArg)
29
+
30
+ // Check if it's a category directory
31
+ if (existsSync(categoryPath) && statSync(categoryPath).isDirectory()) {
32
+ // only run all when flags come before any sub-script name
33
+ const nonFlagArgs = scriptArgs.slice(1).filter((a) => !a.startsWith('--'))
34
+ const hasSubScript = nonFlagArgs.length > 0
35
+ const hasAllFlag = secondArg === '--all' && !hasSubScript
36
+ const hasDefaultAllFlag = scriptArgs.includes('--default-all') && !hasSubScript
37
+
38
+ if (hasAllFlag || hasDefaultAllFlag) {
39
+ // Run all scripts in this category in parallel
40
+ const { discoverScripts } = await import('../utils/script-utils')
41
+ const { runScriptsInParallel } = await import('../utils/parallel-runner')
42
+ const pc = (await import('picocolors')).default
43
+ const categoryScripts = discoverScripts(categoryPath)
44
+
45
+ if (categoryScripts.size === 0) {
46
+ console.info(pc.yellow(`No scripts found in ${firstArg}/`))
47
+ return
48
+ }
49
+
50
+ const scriptsToRun = Array.from(categoryScripts.entries()).map(
51
+ ([name, path]) => ({
52
+ name: name.replace(`${firstArg}/`, ''),
53
+ path,
54
+ })
55
+ )
56
+
57
+ await runScriptsInParallel(scriptsToRun, {
58
+ title: pc.bold(pc.cyan(`Running all scripts in ${firstArg}/`)),
59
+ })
60
+
61
+ return
62
+ }
63
+
64
+ // Lazy load script functions only when needed
65
+ const { discoverScripts, getAllScriptMetadata } =
66
+ await import('../utils/script-utils')
67
+ const pc = (await import('picocolors')).default
68
+
69
+ // List scripts in this category
70
+ const categoryScripts = discoverScripts(categoryPath)
71
+
72
+ if (categoryScripts.size > 0) {
73
+ const metadata = await getAllScriptMetadata(categoryScripts)
74
+
75
+ console.info()
76
+ console.info(pc.bold(pc.cyan(`${firstArg} Scripts`)))
77
+ console.info()
78
+
79
+ for (const [name] of categoryScripts) {
80
+ const shortName = name.replace(`${firstArg}/`, '')
81
+ const meta = metadata.get(name)
82
+ let line = ` ${pc.green(shortName)}`
83
+
84
+ if (meta?.description) {
85
+ line += pc.dim(` - ${meta.description}`)
86
+ }
87
+
88
+ if (meta?.args && meta.args.length > 0) {
89
+ line += pc.dim(` [${meta.args.join(', ')}]`)
90
+ }
91
+
92
+ console.info(line)
93
+ }
94
+
95
+ console.info()
96
+ console.info(pc.dim(`Run: tko ${firstArg} <name> [args...]`))
97
+ console.info(pc.dim(`Or: tko ${firstArg} --all to run all scripts`))
98
+ console.info(pc.dim(`Or: tko run ${firstArg}/<name> [args...] to execute`))
99
+ console.info()
100
+ } else {
101
+ const pc = (await import('picocolors')).default
102
+ console.info(pc.yellow(`No scripts found in ${firstArg}/`))
103
+ }
104
+ return
105
+ }
106
+
107
+ // Check if it's a direct script reference (with slash or colon)
108
+ const normalizedName = firstArg.replace(/:/g, '/')
109
+
110
+ // Lazy load findScript only when needed
111
+ const { findScript } = await import('./script')
112
+ const scriptPath = findScript(normalizedName)
113
+
114
+ if (scriptPath) {
115
+ // Use spawn instead of Bun's $ helper since we're in a compiled context
116
+ const scriptArgsToPass = scriptArgs
117
+ .slice(1)
118
+ .filter((a) => a !== '--default-all' && a !== '--all')
119
+ const child = spawn('bun', [scriptPath, ...scriptArgsToPass], {
120
+ stdio: 'inherit',
121
+ shell: false,
122
+ })
123
+
124
+ const code = await new Promise<number>((resolve) => {
125
+ child.on('exit', (code) => resolve(code || 0))
126
+ })
127
+ process.exit(code)
128
+ }
129
+ }
130
+
131
+ // fall back to runParallelScripts for package.json scripts (direct import, no subprocess)
132
+ const { runParallelScripts, parseRunArgs } = await import('@o/run')
133
+ await runParallelScripts(parseRunArgs(scriptArgs))
134
+ },
135
+ })
@@ -0,0 +1,431 @@
1
+ /**
2
+ * Enhanced Script command - hybrid script runner with filesystem discovery
3
+ */
4
+
5
+ import {
6
+ existsSync,
7
+ readFileSync,
8
+ readdirSync,
9
+ statSync,
10
+ writeFileSync,
11
+ mkdirSync,
12
+ } from 'node:fs'
13
+ import { join, relative, dirname, basename } from 'node:path'
14
+ import { fileURLToPath } from 'node:url'
15
+
16
+ import { defineCommand } from 'citty'
17
+ import pc from 'picocolors'
18
+
19
+ import { listAllScripts, listCategoryScripts } from '../utils/script-listing'
20
+ import { getLocalScriptsDir } from '../utils/script-utils'
21
+ import {
22
+ type FileToSync,
23
+ compareFiles,
24
+ getFileSize,
25
+ syncFileWithConfirmation,
26
+ } from '../utils/sync'
27
+
28
+ // find scripts package root using import.meta.resolve
29
+ function findScriptsPackageRoot(): string | null {
30
+ try {
31
+ const resolved = import.meta.resolve('@o/scripts/package.json')
32
+ // use fileURLToPath for proper cross-platform handling
33
+ const packageJsonPath = fileURLToPath(new URL(resolved))
34
+ const packageRoot = join(packageJsonPath, '..')
35
+ const srcPath = join(packageRoot, 'src')
36
+
37
+ if (existsSync(srcPath)) {
38
+ return srcPath
39
+ }
40
+ } catch (err) {
41
+ // scripts package not found, that's ok for hybrid mode
42
+ }
43
+
44
+ return null
45
+ }
46
+
47
+ // find a script by name (checks both built-in and local)
48
+ export function findScript(name: string): string | null {
49
+ // normalize name: convert colons to slashes for consistency
50
+ const normalizedName = name.replace(/:/g, '/')
51
+
52
+ // 1. check local scripts first (they override built-ins)
53
+ const localDir = getLocalScriptsDir()
54
+
55
+ // try direct file path with normalized name
56
+ for (const ext of ['.ts', '.js', '']) {
57
+ const localPath = join(localDir, `${normalizedName}${ext}`)
58
+ if (existsSync(localPath)) {
59
+ return localPath
60
+ }
61
+ }
62
+
63
+ // 2. check built-in scripts
64
+ const builtInDir = findScriptsPackageRoot()
65
+ if (builtInDir) {
66
+ for (const ext of ['.ts', '.js', '']) {
67
+ const builtInPath = join(builtInDir, `${normalizedName}${ext}`)
68
+ if (existsSync(builtInPath)) {
69
+ return builtInPath
70
+ }
71
+ }
72
+ }
73
+
74
+ return null
75
+ }
76
+
77
+ export { listAllScripts, listCategoryScripts } from '../utils/script-listing'
78
+
79
+ // run a script
80
+ async function runScript(scriptPath: string, args: string[]): Promise<void> {
81
+ console.info(pc.dim(`Running: ${relative(process.cwd(), scriptPath)}`))
82
+ console.info()
83
+
84
+ try {
85
+ const { $ } = await import('bun')
86
+ await $`bun ${scriptPath} ${args}`
87
+ } catch (err) {
88
+ console.error(pc.red(`✗ Error running script: ${err}`))
89
+ process.exit(1)
90
+ }
91
+ }
92
+
93
+ // new command for creating scripts
94
+ const newCommand = defineCommand({
95
+ meta: {
96
+ name: 'new',
97
+ description: 'Create a new script from template',
98
+ },
99
+ args: {
100
+ path: {
101
+ type: 'positional',
102
+ description: 'Path for the new script (e.g., ci/test)',
103
+ required: true,
104
+ },
105
+ },
106
+ async run({ args }) {
107
+ const scriptPath = args.path
108
+ const localDir = getLocalScriptsDir()
109
+
110
+ // determine full path
111
+ const fullPath = join(localDir, `${scriptPath}.ts`)
112
+ const dir = dirname(fullPath)
113
+
114
+ // create directory if needed
115
+ if (!existsSync(dir)) {
116
+ mkdirSync(dir, { recursive: true })
117
+ }
118
+
119
+ // check if file exists
120
+ if (existsSync(fullPath)) {
121
+ console.error(
122
+ pc.red(`✗ Script already exists: ${relative(process.cwd(), fullPath)}`)
123
+ )
124
+ process.exit(1)
125
+ }
126
+
127
+ // create template
128
+ const template = `#!/usr/bin/env bun
129
+
130
+ /**
131
+ * @description TODO: Add description
132
+ * @args --verbose, --dry-run
133
+ */
134
+
135
+ import { args } from '@o/scripts/helpers/args'
136
+
137
+ const { verbose, dryRun } = args('--verbose boolean --dry-run boolean')
138
+
139
+ async function main() {
140
+ console.info('Running ${basename(scriptPath)}...')
141
+
142
+ if (verbose) {
143
+ console.info('Verbose mode enabled')
144
+ }
145
+
146
+ if (dryRun) {
147
+ console.info('Dry run mode - no changes will be made')
148
+ }
149
+
150
+ // TODO: Implement your script logic here
151
+ }
152
+
153
+ main().catch(console.error)
154
+ `
155
+
156
+ writeFileSync(fullPath, template)
157
+ console.info(pc.green(`✓ Created new script: ${relative(process.cwd(), fullPath)}`))
158
+ console.info()
159
+ console.info(pc.dim(`Edit the script and update the TODO sections`))
160
+ },
161
+ })
162
+
163
+ // get all script files recursively
164
+ function getAllScriptFiles(dir: string, baseDir: string = dir): string[] {
165
+ const files: string[] = []
166
+
167
+ if (!existsSync(dir)) {
168
+ return files
169
+ }
170
+
171
+ try {
172
+ const entries = readdirSync(dir)
173
+
174
+ for (const entry of entries) {
175
+ const fullPath = join(dir, entry)
176
+ const stat = statSync(fullPath)
177
+
178
+ if (stat.isDirectory()) {
179
+ files.push(...getAllScriptFiles(fullPath, baseDir))
180
+ } else if (
181
+ entry.endsWith('.ts') ||
182
+ entry.endsWith('.js') ||
183
+ entry.endsWith('.cjs')
184
+ ) {
185
+ // normalize to forward slashes for consistent naming
186
+ files.push(relative(baseDir, fullPath).split('\\').join('/'))
187
+ }
188
+ }
189
+ } catch (err) {
190
+ // ignore directory read errors
191
+ }
192
+
193
+ return files
194
+ }
195
+
196
+ // eject command to copy built-in scripts to local directory
197
+ const ejectCommand = defineCommand({
198
+ meta: {
199
+ name: 'eject',
200
+ description: 'Eject built-in scripts into your project',
201
+ },
202
+ args: {
203
+ yes: {
204
+ type: 'boolean',
205
+ description: 'Skip confirmations and eject all files',
206
+ default: false,
207
+ },
208
+ },
209
+ async run({ args }) {
210
+ const cwd = process.cwd()
211
+ const targetScriptsDir = join(cwd, 'scripts')
212
+ const sourceScriptsDir = findScriptsPackageRoot()
213
+
214
+ console.info()
215
+ console.info(pc.bold(pc.cyan('⚙️ Eject Scripts')))
216
+ console.info()
217
+
218
+ if (!sourceScriptsDir) {
219
+ console.error(
220
+ pc.red('✗ Built-in scripts package (@o/scripts) not found or not installed')
221
+ )
222
+ console.info()
223
+ console.info(pc.dim('Install with: bun add -d @o/scripts'))
224
+ process.exit(1)
225
+ }
226
+
227
+ console.info(pc.dim(`Source: ${sourceScriptsDir}`))
228
+ console.info(pc.dim(`Target: ${targetScriptsDir}`))
229
+ console.info()
230
+
231
+ // ensure target scripts directory exists
232
+ if (!existsSync(targetScriptsDir)) {
233
+ console.info(pc.yellow('⚠ Target scripts directory does not exist, will create it'))
234
+ mkdirSync(targetScriptsDir, { recursive: true })
235
+ }
236
+
237
+ // get all script files from source (including subdirectories)
238
+ const sourceFiles = getAllScriptFiles(sourceScriptsDir)
239
+
240
+ if (sourceFiles.length === 0) {
241
+ console.info(pc.yellow('No script files found in built-in scripts package'))
242
+ return
243
+ }
244
+
245
+ // analyze all files
246
+ const filesToSync: FileToSync[] = []
247
+ const stats = {
248
+ new: 0,
249
+ modified: 0,
250
+ identical: 0,
251
+ }
252
+
253
+ for (const file of sourceFiles) {
254
+ const sourcePath = join(sourceScriptsDir, file)
255
+ const targetPath = join(targetScriptsDir, file)
256
+ const status = compareFiles(sourcePath, targetPath)
257
+
258
+ stats[status]++
259
+
260
+ filesToSync.push({
261
+ name: file,
262
+ sourcePath,
263
+ targetPath,
264
+ status,
265
+ sourceSize: getFileSize(sourcePath),
266
+ targetSize: getFileSize(targetPath),
267
+ })
268
+ }
269
+
270
+ console.info(pc.bold('Summary:'))
271
+ console.info(` ${pc.green(`${stats.new} new`)}`)
272
+ console.info(` ${pc.yellow(`${stats.modified} modified`)}`)
273
+ console.info(` ${pc.dim(`${stats.identical} identical`)}`)
274
+ console.info()
275
+
276
+ if (stats.new === 0 && stats.modified === 0) {
277
+ console.info(pc.green('✓ All scripts are already up to date!'))
278
+ return
279
+ }
280
+
281
+ // sort files: new first, then modified, then identical
282
+ const sortOrder = { new: 0, modified: 1, identical: 2 }
283
+ filesToSync.sort((a, b) => sortOrder[a.status] - sortOrder[b.status])
284
+
285
+ // sync files
286
+ let syncedCount = 0
287
+
288
+ for (const file of filesToSync) {
289
+ if (args.yes && file.status !== 'identical') {
290
+ // auto-sync without confirmation
291
+ const targetDir = dirname(file.targetPath)
292
+ if (!existsSync(targetDir)) {
293
+ mkdirSync(targetDir, { recursive: true })
294
+ }
295
+ const content = readFileSync(file.sourcePath)
296
+ writeFileSync(file.targetPath, content)
297
+ console.info(pc.green(` ✓ ${file.name}`))
298
+ syncedCount++
299
+ } else {
300
+ const wasSynced = await syncFileWithConfirmation(file)
301
+ if (wasSynced) {
302
+ syncedCount++
303
+ }
304
+ }
305
+ }
306
+
307
+ console.info()
308
+ console.info(pc.bold(pc.green(`✓ Complete: ${syncedCount} file(s) ejected`)))
309
+ console.info()
310
+ },
311
+ })
312
+
313
+ // run subcommand for backwards compatibility
314
+ const runSubCommand = defineCommand({
315
+ meta: {
316
+ name: 'run',
317
+ description: 'Run a script (for backwards compatibility)',
318
+ },
319
+ args: {
320
+ name: {
321
+ type: 'positional',
322
+ description: 'Script name',
323
+ required: true,
324
+ },
325
+ },
326
+ async run({ args, rawArgs }) {
327
+ // Find and run the script
328
+ const scriptPath = findScript(args.name)
329
+
330
+ if (!scriptPath) {
331
+ console.error(pc.red(`✗ Script not found: ${args.name}`))
332
+ console.info()
333
+ console.info(pc.dim(`Run 'tko script' to see available scripts`))
334
+ process.exit(1)
335
+ }
336
+
337
+ // Extract args after the script name
338
+ const scriptArgs = rawArgs.slice(rawArgs.indexOf(args.name) + 1)
339
+
340
+ await runScript(scriptPath, scriptArgs)
341
+ },
342
+ })
343
+
344
+ // main script command (handles both list and run)
345
+ export const scriptCommand = defineCommand({
346
+ meta: {
347
+ name: 'script',
348
+ description: 'Hybrid script runner with filesystem discovery',
349
+ },
350
+ args: {
351
+ name: {
352
+ type: 'positional',
353
+ description: 'Script name or category to run/list',
354
+ required: false,
355
+ },
356
+ },
357
+ subCommands: {
358
+ new: newCommand,
359
+ run: runSubCommand,
360
+ eject: ejectCommand,
361
+ },
362
+ async run({ args, rawArgs }) {
363
+ // citty doesn't handle args with hyphens well, so use rawArgs directly
364
+ // rawArgs[0] is 'script', rawArgs[1] is the actual script name
365
+ const scriptName = args.name || rawArgs[1]
366
+
367
+ // if no name provided, list all available scripts
368
+ if (!scriptName) {
369
+ await listAllScripts(false)
370
+ return
371
+ }
372
+
373
+ // check if this is a category listing
374
+ if (await listCategoryScripts(scriptName)) {
375
+ return
376
+ }
377
+
378
+ // find and run the script
379
+ const scriptPath = findScript(scriptName)
380
+
381
+ if (!scriptPath) {
382
+ console.error(pc.red(`✗ Script not found: ${scriptName}`))
383
+ console.info()
384
+ console.info(pc.dim(`Run 'tko script' to see available scripts`))
385
+ process.exit(1)
386
+ }
387
+
388
+ // extract args after the script name
389
+ const scriptArgs = rawArgs.slice(rawArgs.indexOf(scriptName) + 1)
390
+
391
+ await runScript(scriptPath, scriptArgs)
392
+ },
393
+ })
394
+
395
+ // shorthand command that skips "script" subcommand
396
+ export function createShorthandCommand(name: string) {
397
+ return defineCommand({
398
+ meta: {
399
+ name,
400
+ description: 'Run script directly',
401
+ },
402
+ args: {
403
+ args: {
404
+ type: 'positional',
405
+ description: 'Script and arguments',
406
+ required: false,
407
+ },
408
+ },
409
+ async run({ rawArgs }) {
410
+ // if no args provided, list scripts in this category
411
+ const scriptName = rawArgs.length > 0 ? `${name}/${rawArgs[0]}` : name
412
+ const scriptArgs =
413
+ rawArgs.length > 0
414
+ ? ['script', scriptName, ...rawArgs.slice(1)]
415
+ : ['script', name]
416
+
417
+ // delegate to script command
418
+ const { run } = scriptCommand
419
+ if (run) {
420
+ await run({
421
+ args: {
422
+ name: scriptName,
423
+ _: rawArgs.slice(1),
424
+ },
425
+ rawArgs: scriptArgs,
426
+ cmd: scriptCommand,
427
+ })
428
+ }
429
+ },
430
+ })
431
+ }