@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.
- package/dist/cmd.d.ts +10 -8
- package/dist/cmd.d.ts.map +1 -1
- package/dist/cmd.js.map +1 -1
- package/package.json +31 -6
- package/src/tko/cli.ts +173 -0
- package/src/tko/commands/changed.ts +313 -0
- package/src/tko/commands/docs.ts +164 -0
- package/src/tko/commands/env-setup.ts +69 -0
- package/src/tko/commands/onboard.ts +1420 -0
- package/src/tko/commands/run-all.ts +83 -0
- package/src/tko/commands/run.ts +135 -0
- package/src/tko/commands/script.ts +431 -0
- package/src/tko/commands/skills.test.ts +193 -0
- package/src/tko/commands/skills.ts +361 -0
- package/src/tko/commands/sync.ts +319 -0
- package/src/tko/constants/ascii.ts +17 -0
- package/src/tko/index.ts +1 -0
- package/src/tko/types.ts +59 -0
- package/src/tko/utils/env-categories.ts +246 -0
- package/src/tko/utils/env-setup.ts +335 -0
- package/src/tko/utils/env.ts +127 -0
- package/src/tko/utils/files.ts +302 -0
- package/src/tko/utils/parallel-runner.ts +195 -0
- package/src/tko/utils/ports.ts +77 -0
- package/src/tko/utils/prerequisites.ts +137 -0
- package/src/tko/utils/prompts.ts +203 -0
- package/src/tko/utils/script-listing.ts +194 -0
- package/src/tko/utils/script-utils.ts +103 -0
- package/src/tko/utils/sync.ts +101 -0
- package/src/tko/withExpoModulesCoreSwift6Fix.cjs +58 -0
- package/src/tko/withOpSqliteStatic.cjs +51 -0
|
@@ -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
|
+
}
|