@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,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reusable prompt helpers using @clack/prompts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as p from '@clack/prompts'
|
|
6
|
+
import pc from 'picocolors'
|
|
7
|
+
|
|
8
|
+
import type { PortCheck, PrerequisiteCheck } from '../types'
|
|
9
|
+
|
|
10
|
+
export function displayWelcome(projectName = 'Takeout'): void {
|
|
11
|
+
console.info()
|
|
12
|
+
p.intro(pc.bgCyan(pc.black(` ${projectName} Starter Kit `)))
|
|
13
|
+
console.info()
|
|
14
|
+
p.note(
|
|
15
|
+
pc.dim(
|
|
16
|
+
"You can re-run 'bun onboard' anytime to reconfigure or skip to specific steps.\nNo need to decide everything upfront!"
|
|
17
|
+
),
|
|
18
|
+
pc.cyan('Tip')
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function displayOutro(message: string): void {
|
|
23
|
+
p.outro(pc.green(message))
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function displayPrerequisites(checks: PrerequisiteCheck[]): void {
|
|
27
|
+
const items = checks.map((check) => {
|
|
28
|
+
const icon = check.installed ? pc.green('✓') : pc.red('✗')
|
|
29
|
+
const message = check.message || ''
|
|
30
|
+
const recommendation = check.recommendation
|
|
31
|
+
? `\n ${pc.dim(check.recommendation)}`
|
|
32
|
+
: ''
|
|
33
|
+
return `${icon} ${pc.bold(check.name)}: ${message}${recommendation}`
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
p.note(items.join('\n'), 'Prerequisites')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function displayPortConflicts(conflicts: PortCheck[]): void {
|
|
40
|
+
if (conflicts.length === 0) return
|
|
41
|
+
|
|
42
|
+
const items = conflicts.map((conflict) => {
|
|
43
|
+
const pid = conflict.pid ? ` (PID: ${conflict.pid})` : ''
|
|
44
|
+
return `${pc.yellow('⚠')} Port ${conflict.port} (${conflict.name})${pid}`
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
p.note(items.join('\n'), pc.yellow('Port Conflicts Detected'))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function confirmContinue(
|
|
51
|
+
message: string,
|
|
52
|
+
defaultValue = true
|
|
53
|
+
): Promise<boolean> {
|
|
54
|
+
const result = await p.confirm({
|
|
55
|
+
message,
|
|
56
|
+
initialValue: defaultValue,
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if (p.isCancel(result)) {
|
|
60
|
+
p.cancel('Operation cancelled.')
|
|
61
|
+
process.exit(0)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return result
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export async function promptText(
|
|
68
|
+
message: string,
|
|
69
|
+
defaultValue?: string,
|
|
70
|
+
placeholder?: string
|
|
71
|
+
): Promise<string> {
|
|
72
|
+
const result = await p.text({
|
|
73
|
+
message,
|
|
74
|
+
defaultValue,
|
|
75
|
+
placeholder,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
if (p.isCancel(result)) {
|
|
79
|
+
p.cancel('Operation cancelled.')
|
|
80
|
+
process.exit(0)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function promptPassword(message: string): Promise<string> {
|
|
87
|
+
const result = await p.password({
|
|
88
|
+
message,
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
if (p.isCancel(result)) {
|
|
92
|
+
p.cancel('Operation cancelled.')
|
|
93
|
+
process.exit(0)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return result
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function promptSelect<T extends string>(
|
|
100
|
+
message: string,
|
|
101
|
+
options: { value: T; label: string; hint?: string }[]
|
|
102
|
+
): Promise<T | 'cancel'> {
|
|
103
|
+
const result = await p.select({
|
|
104
|
+
message,
|
|
105
|
+
options: options as any,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
if (p.isCancel(result)) {
|
|
109
|
+
return 'cancel' as const
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return result as T
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export async function promptStartStep(
|
|
116
|
+
hasLocalPackages = true
|
|
117
|
+
): Promise<
|
|
118
|
+
'full' | 'prerequisites' | 'identity' | 'ports' | 'eject' | 'production' | 'cancel'
|
|
119
|
+
> {
|
|
120
|
+
console.info()
|
|
121
|
+
console.info(pc.gray('What would you like to do?'))
|
|
122
|
+
console.info()
|
|
123
|
+
|
|
124
|
+
return promptSelect('Select starting point:', [
|
|
125
|
+
{
|
|
126
|
+
value: 'full',
|
|
127
|
+
label: 'Complete setup',
|
|
128
|
+
hint: 'Run through all setup steps',
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
value: 'prerequisites',
|
|
132
|
+
label: 'Check prerequisites',
|
|
133
|
+
hint: 'Verify bun, docker, git, etc.',
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
value: 'identity',
|
|
137
|
+
label: 'Project identity',
|
|
138
|
+
hint: 'Customize app name and bundle ID',
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
value: 'ports',
|
|
142
|
+
label: 'Check ports',
|
|
143
|
+
hint: 'Verify required ports are available',
|
|
144
|
+
},
|
|
145
|
+
...(hasLocalPackages
|
|
146
|
+
? [
|
|
147
|
+
{
|
|
148
|
+
value: 'eject' as const,
|
|
149
|
+
label: 'Eject from monorepo',
|
|
150
|
+
hint: 'Remove ./packages, use published versions',
|
|
151
|
+
},
|
|
152
|
+
]
|
|
153
|
+
: []),
|
|
154
|
+
{
|
|
155
|
+
value: 'production',
|
|
156
|
+
label: 'Production deployment',
|
|
157
|
+
hint: 'Configure production environment and deployment',
|
|
158
|
+
},
|
|
159
|
+
])
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export async function promptOldSelect<T extends string>(
|
|
163
|
+
message: string,
|
|
164
|
+
options: { value: T; label: string; hint?: string }[]
|
|
165
|
+
): Promise<T> {
|
|
166
|
+
const result = await p.select<string>({
|
|
167
|
+
message,
|
|
168
|
+
options,
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
if (p.isCancel(result)) {
|
|
172
|
+
p.cancel('Operation cancelled.')
|
|
173
|
+
process.exit(0)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result as T
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function showSpinner(message: string): ReturnType<typeof p.spinner> {
|
|
180
|
+
const s = p.spinner()
|
|
181
|
+
s.start(message)
|
|
182
|
+
return s
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function showError(message: string): void {
|
|
186
|
+
p.log.error(pc.red(message))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function showWarning(message: string): void {
|
|
190
|
+
p.log.warning(pc.yellow(message))
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function showSuccess(message: string): void {
|
|
194
|
+
p.log.success(pc.green(message))
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
export function showInfo(message: string): void {
|
|
198
|
+
p.log.info(pc.blue(message))
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export function showStep(message: string): void {
|
|
202
|
+
p.log.step(message)
|
|
203
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for listing and discovering scripts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, statSync } from 'node:fs'
|
|
6
|
+
import { join, relative } from 'node:path'
|
|
7
|
+
import { fileURLToPath } from 'node:url'
|
|
8
|
+
|
|
9
|
+
import pc from 'picocolors'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
type ScriptMetadata,
|
|
13
|
+
discoverScripts,
|
|
14
|
+
getAllScriptMetadata,
|
|
15
|
+
getLocalScriptsDir,
|
|
16
|
+
} from './script-utils'
|
|
17
|
+
|
|
18
|
+
// find scripts package root using import.meta.resolve
|
|
19
|
+
function findScriptsPackageRoot(): string | null {
|
|
20
|
+
try {
|
|
21
|
+
const resolved = import.meta.resolve('@o/scripts/package.json')
|
|
22
|
+
const packageJsonPath = fileURLToPath(new URL(resolved))
|
|
23
|
+
const packageRoot = join(packageJsonPath, '..')
|
|
24
|
+
const srcPath = join(packageRoot, 'src')
|
|
25
|
+
|
|
26
|
+
if (existsSync(srcPath)) {
|
|
27
|
+
return srcPath
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// scripts package not found
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// format script listing with pre-fetched metadata
|
|
37
|
+
function formatScriptList(
|
|
38
|
+
title: string,
|
|
39
|
+
scripts: Map<string, string>,
|
|
40
|
+
metadata: Map<string, ScriptMetadata>
|
|
41
|
+
): void {
|
|
42
|
+
if (scripts.size === 0) return
|
|
43
|
+
|
|
44
|
+
console.info()
|
|
45
|
+
console.info(pc.bold(pc.cyan(title)))
|
|
46
|
+
console.info()
|
|
47
|
+
|
|
48
|
+
const categories = new Map<string, Array<[string, string]>>()
|
|
49
|
+
const rootScripts: Array<[string, string]> = []
|
|
50
|
+
|
|
51
|
+
for (const [name, path] of scripts) {
|
|
52
|
+
if (name.includes('/')) {
|
|
53
|
+
const category = name.split('/')[0]!
|
|
54
|
+
if (!categories.has(category)) {
|
|
55
|
+
categories.set(category, [])
|
|
56
|
+
}
|
|
57
|
+
categories.get(category)!.push([name, path])
|
|
58
|
+
} else {
|
|
59
|
+
rootScripts.push([name, path])
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
for (const [name] of rootScripts) {
|
|
64
|
+
let line = ` ${pc.green(name)}`
|
|
65
|
+
const meta = metadata.get(name)
|
|
66
|
+
if (meta?.description) {
|
|
67
|
+
line += pc.dim(` - ${meta.description}`)
|
|
68
|
+
}
|
|
69
|
+
console.info(line)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
for (const [category, categoryScripts] of categories) {
|
|
73
|
+
console.info()
|
|
74
|
+
console.info(` ${pc.yellow(category)}/`)
|
|
75
|
+
|
|
76
|
+
for (const [name] of categoryScripts) {
|
|
77
|
+
const shortName = name.substring(category.length + 1)
|
|
78
|
+
let line = ` ${pc.green(shortName)}`
|
|
79
|
+
const meta = metadata.get(name)
|
|
80
|
+
if (meta?.description) {
|
|
81
|
+
line += pc.dim(` - ${meta.description}`)
|
|
82
|
+
}
|
|
83
|
+
console.info(line)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// list all available scripts
|
|
89
|
+
export async function listAllScripts(includeCommands = true) {
|
|
90
|
+
console.info()
|
|
91
|
+
console.info(pc.bold(pc.cyan('Takeout CLI - Project Scripts & Commands')))
|
|
92
|
+
console.info()
|
|
93
|
+
console.info(pc.dim(' Manage scripts, run tasks, and configure your project'))
|
|
94
|
+
console.info()
|
|
95
|
+
|
|
96
|
+
if (includeCommands) {
|
|
97
|
+
console.info(pc.bold(pc.cyan('Built-in Commands')))
|
|
98
|
+
console.info()
|
|
99
|
+
console.info(` ${pc.green('onboard')} - Setup wizard for new projects`)
|
|
100
|
+
console.info(` ${pc.green('docs')} - View documentation`)
|
|
101
|
+
console.info(` ${pc.green('env:setup')} - Setup environment variables`)
|
|
102
|
+
console.info(` ${pc.green('run')} - Run scripts in parallel`)
|
|
103
|
+
console.info(` ${pc.green('script')} - Manage and run scripts`)
|
|
104
|
+
console.info(` ${pc.green('skills')} - Manage Claude Code skills`)
|
|
105
|
+
console.info(` ${pc.green('sync')} - Sync fork with upstream Takeout`)
|
|
106
|
+
console.info(` ${pc.green('changed')} - Show changes since last sync`)
|
|
107
|
+
console.info(` ${pc.green('completion')} - Shell completion setup`)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const localScripts = discoverScripts(getLocalScriptsDir())
|
|
111
|
+
const builtInDir = findScriptsPackageRoot()
|
|
112
|
+
const builtInScripts = builtInDir ? discoverScripts(builtInDir) : new Map()
|
|
113
|
+
|
|
114
|
+
const allScripts = new Map([...localScripts, ...builtInScripts])
|
|
115
|
+
const metadata = await getAllScriptMetadata(allScripts)
|
|
116
|
+
|
|
117
|
+
if (localScripts.size > 0) {
|
|
118
|
+
formatScriptList('Local Scripts', localScripts, metadata)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (builtInScripts.size > 0) {
|
|
122
|
+
formatScriptList('Built-in Scripts', builtInScripts, metadata)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (localScripts.size === 0 && builtInScripts.size === 0) {
|
|
126
|
+
console.info()
|
|
127
|
+
console.info(pc.yellow('No scripts found'))
|
|
128
|
+
console.info()
|
|
129
|
+
console.info(
|
|
130
|
+
pc.dim(`Create scripts in ${relative(process.cwd(), getLocalScriptsDir())}/`)
|
|
131
|
+
)
|
|
132
|
+
console.info(pc.dim(`Or install @o/scripts package for built-in scripts`))
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
console.info()
|
|
136
|
+
console.info(pc.bold('Usage:'))
|
|
137
|
+
console.info(
|
|
138
|
+
` ${pc.cyan('tko <command>')} ${pc.dim('Run a built-in command')}`
|
|
139
|
+
)
|
|
140
|
+
console.info(
|
|
141
|
+
` ${pc.cyan('tko <script-name>')} ${pc.dim('Execute direct script')}`
|
|
142
|
+
)
|
|
143
|
+
console.info(
|
|
144
|
+
` ${pc.cyan('tko <group> <script>')} ${pc.dim('Execute nested script')}`
|
|
145
|
+
)
|
|
146
|
+
console.info(` ${pc.cyan('tko script new <path>')} ${pc.dim('Create a new script')}`)
|
|
147
|
+
console.info()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// check if a name is a category and list its scripts
|
|
151
|
+
export async function listCategoryScripts(categoryName: string): Promise<boolean> {
|
|
152
|
+
const localDir = getLocalScriptsDir()
|
|
153
|
+
const categoryPath = join(localDir, categoryName)
|
|
154
|
+
|
|
155
|
+
if (existsSync(categoryPath) && statSync(categoryPath).isDirectory()) {
|
|
156
|
+
const categoryScripts = discoverScripts(categoryPath)
|
|
157
|
+
|
|
158
|
+
if (categoryScripts.size > 0) {
|
|
159
|
+
const metadata = await getAllScriptMetadata(categoryScripts)
|
|
160
|
+
|
|
161
|
+
console.info()
|
|
162
|
+
console.info(pc.bold(pc.cyan(`${categoryName} Scripts`)))
|
|
163
|
+
console.info()
|
|
164
|
+
|
|
165
|
+
for (const [name] of categoryScripts) {
|
|
166
|
+
const shortName = name.replace(`${categoryName}/`, '')
|
|
167
|
+
const meta = metadata.get(name)
|
|
168
|
+
let line = ` ${pc.green(shortName)}`
|
|
169
|
+
|
|
170
|
+
if (meta?.description) {
|
|
171
|
+
line += pc.dim(` - ${meta.description}`)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (meta?.args && meta.args.length > 0) {
|
|
175
|
+
line += pc.dim(` [${meta.args.join(', ')}]`)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.info(line)
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.info()
|
|
182
|
+
console.info(pc.dim(`Run: tko ${categoryName}/<name> [args...] to execute`))
|
|
183
|
+
console.info()
|
|
184
|
+
} else {
|
|
185
|
+
console.info()
|
|
186
|
+
console.info(pc.yellow(`No scripts found in ${categoryName}/`))
|
|
187
|
+
console.info()
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return true
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return false
|
|
194
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core utility functions for script discovery and metadata
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
|
|
6
|
+
import { join, relative } from 'node:path'
|
|
7
|
+
|
|
8
|
+
import { setInterceptCmd } from '@o/scripts/cmd'
|
|
9
|
+
|
|
10
|
+
export interface ScriptMetadata {
|
|
11
|
+
description?: string
|
|
12
|
+
args?: string[]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// get local scripts directory
|
|
16
|
+
export function getLocalScriptsDir(): string {
|
|
17
|
+
return join(process.cwd(), 'scripts')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// batch extract metadata for all scripts via import interception
|
|
21
|
+
export async function getAllScriptMetadata(
|
|
22
|
+
scripts: Map<string, string>
|
|
23
|
+
): Promise<Map<string, ScriptMetadata>> {
|
|
24
|
+
const collected = new Map<string, { description: string; args?: string }>()
|
|
25
|
+
let currentName = ''
|
|
26
|
+
|
|
27
|
+
// interceptor stays set — never cleared so floating .run() promises always bail
|
|
28
|
+
setInterceptCmd((info) => {
|
|
29
|
+
collected.set(currentName, info)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
for (const [name, path] of scripts) {
|
|
33
|
+
// only import scripts that use cmd (safe to import)
|
|
34
|
+
try {
|
|
35
|
+
const head = readFileSync(path, 'utf-8').slice(0, 200)
|
|
36
|
+
if (!head.includes('cmd')) continue
|
|
37
|
+
} catch {
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
currentName = name
|
|
42
|
+
try {
|
|
43
|
+
await import(path)
|
|
44
|
+
} catch {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const results = new Map<string, ScriptMetadata>()
|
|
48
|
+
for (const [name, info] of collected) {
|
|
49
|
+
const metadata: ScriptMetadata = { description: info.description }
|
|
50
|
+
if (info.args) {
|
|
51
|
+
const flags = [...info.args.matchAll(/--?([a-z-]+)/gi)]
|
|
52
|
+
if (flags.length > 0) {
|
|
53
|
+
metadata.args = flags.map((m) => `--${m[1]}`)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
results.set(name, metadata)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return results
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// discover scripts in a directory (no grandchildren)
|
|
63
|
+
export function discoverScripts(dir: string, baseDir: string = dir): Map<string, string> {
|
|
64
|
+
const scripts = new Map<string, string>()
|
|
65
|
+
|
|
66
|
+
if (!existsSync(dir)) {
|
|
67
|
+
return scripts
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const entries = readdirSync(dir)
|
|
72
|
+
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
const fullPath = join(dir, entry)
|
|
75
|
+
const stat = statSync(fullPath)
|
|
76
|
+
|
|
77
|
+
if (stat.isDirectory()) {
|
|
78
|
+
if (entry === 'helpers' || entry === 'internal') continue
|
|
79
|
+
// one level deep only
|
|
80
|
+
const subEntries = readdirSync(fullPath)
|
|
81
|
+
for (const subEntry of subEntries) {
|
|
82
|
+
const subPath = join(fullPath, subEntry)
|
|
83
|
+
if (
|
|
84
|
+
statSync(subPath).isFile() &&
|
|
85
|
+
(subEntry.endsWith('.ts') || subEntry.endsWith('.js'))
|
|
86
|
+
) {
|
|
87
|
+
const relativePath = relative(baseDir, subPath).split('\\').join('/')
|
|
88
|
+
const scriptName = relativePath.replace(/\.(ts|js)$/, '')
|
|
89
|
+
scripts.set(scriptName, subPath)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} else if (entry.endsWith('.ts') || entry.endsWith('.js')) {
|
|
93
|
+
const relativePath = relative(baseDir, fullPath).split('\\').join('/')
|
|
94
|
+
const scriptName = relativePath.replace(/\.(ts|js)$/, '')
|
|
95
|
+
scripts.set(scriptName, fullPath)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
} catch {
|
|
99
|
+
// ignore directory read errors
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return scripts
|
|
103
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync utilities for updating docs and scripts between repositories
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createHash } from 'node:crypto'
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, statSync } from 'node:fs'
|
|
7
|
+
import { join, relative, dirname } from 'node:path'
|
|
8
|
+
|
|
9
|
+
import { confirm } from '@clack/prompts'
|
|
10
|
+
import pc from 'picocolors'
|
|
11
|
+
|
|
12
|
+
export interface FileToSync {
|
|
13
|
+
name: string
|
|
14
|
+
sourcePath: string
|
|
15
|
+
targetPath: string
|
|
16
|
+
status: 'new' | 'modified' | 'identical'
|
|
17
|
+
sourceSize?: number
|
|
18
|
+
targetSize?: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getFileHash(filePath: string): string {
|
|
22
|
+
const content = readFileSync(filePath)
|
|
23
|
+
return createHash('md5').update(content).digest('hex')
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function compareFiles(
|
|
27
|
+
sourcePath: string,
|
|
28
|
+
targetPath: string
|
|
29
|
+
): 'new' | 'modified' | 'identical' {
|
|
30
|
+
if (!existsSync(targetPath)) {
|
|
31
|
+
return 'new'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sourceHash = getFileHash(sourcePath)
|
|
35
|
+
const targetHash = getFileHash(targetPath)
|
|
36
|
+
|
|
37
|
+
return sourceHash === targetHash ? 'identical' : 'modified'
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function syncFileWithConfirmation(file: FileToSync): Promise<boolean> {
|
|
41
|
+
const relPath = relative(process.cwd(), file.targetPath)
|
|
42
|
+
|
|
43
|
+
if (file.status === 'identical') {
|
|
44
|
+
console.info(pc.dim(` ✓ ${file.name} (already up to date)`))
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const statusColor = file.status === 'new' ? pc.green : pc.yellow
|
|
49
|
+
const statusText = file.status === 'new' ? 'NEW' : 'MODIFIED'
|
|
50
|
+
|
|
51
|
+
console.info()
|
|
52
|
+
console.info(statusColor(` ${statusText}: ${file.name}`))
|
|
53
|
+
|
|
54
|
+
if (file.status === 'modified') {
|
|
55
|
+
const sourceSize = file.sourceSize || 0
|
|
56
|
+
const targetSize = file.targetSize || 0
|
|
57
|
+
const diff = sourceSize - targetSize
|
|
58
|
+
|
|
59
|
+
console.info(pc.dim(` Source: ${sourceSize} bytes`))
|
|
60
|
+
console.info(pc.dim(` Target: ${targetSize} bytes`))
|
|
61
|
+
|
|
62
|
+
if (diff > 0) {
|
|
63
|
+
console.info(pc.cyan(` Source is ${diff} bytes larger`))
|
|
64
|
+
} else if (diff < 0) {
|
|
65
|
+
console.info(pc.magenta(` Target is ${Math.abs(diff)} bytes larger`))
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
console.info(pc.dim(` → ${relPath}`))
|
|
70
|
+
|
|
71
|
+
const shouldSync = await confirm({
|
|
72
|
+
message: `Overwrite this file?`,
|
|
73
|
+
initialValue: true,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
if (shouldSync === false || typeof shouldSync === 'symbol') {
|
|
77
|
+
console.info(pc.dim(` ⊘ Skipped`))
|
|
78
|
+
return false
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ensure target directory exists
|
|
82
|
+
const targetDir = dirname(file.targetPath)
|
|
83
|
+
if (!existsSync(targetDir)) {
|
|
84
|
+
mkdirSync(targetDir, { recursive: true })
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// copy file
|
|
88
|
+
const content = readFileSync(file.sourcePath)
|
|
89
|
+
writeFileSync(file.targetPath, content)
|
|
90
|
+
|
|
91
|
+
console.info(pc.green(` ✓ Synced`))
|
|
92
|
+
return true
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function getFileSize(filePath: string): number {
|
|
96
|
+
try {
|
|
97
|
+
return statSync(filePath).size
|
|
98
|
+
} catch {
|
|
99
|
+
return 0
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const { withDangerousMod } = require('@expo/config-plugins')
|
|
2
|
+
const fs = require('node:fs')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* expo config plugin to fix expo-modules-core 55.x swift 6 strict concurrency errors
|
|
7
|
+
* that break TurboModule registration (PlatformConstants not found)
|
|
8
|
+
* adapted from tamagui/kitchen-sink
|
|
9
|
+
* see: https://github.com/expo/expo/issues/42525
|
|
10
|
+
*/
|
|
11
|
+
function withExpoModulesCoreSwift6Fix(config) {
|
|
12
|
+
return withDangerousMod(config, [
|
|
13
|
+
'ios',
|
|
14
|
+
async (config) => {
|
|
15
|
+
const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile')
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(podfilePath)) {
|
|
18
|
+
return config
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let podfile = fs.readFileSync(podfilePath, 'utf8')
|
|
22
|
+
|
|
23
|
+
if (podfile.includes('# workaround: expo-modules-core 55.x')) {
|
|
24
|
+
return config
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const workaround = `
|
|
28
|
+
# workaround: expo-modules-core 55.x requires Swift 6 mode with isolated
|
|
29
|
+
# conformances (SE-0470) for @MainActor in protocol conformance syntax.
|
|
30
|
+
# SWIFT_STRICT_CONCURRENCY=minimal suppresses concurrency warnings/errors.
|
|
31
|
+
installer.pods_project.targets.each do |target|
|
|
32
|
+
if target.name == 'ExpoModulesCore'
|
|
33
|
+
target.build_configurations.each do |build_config|
|
|
34
|
+
build_config.build_settings['SWIFT_VERSION'] = '6'
|
|
35
|
+
build_config.build_settings['SWIFT_STRICT_CONCURRENCY'] = 'minimal'
|
|
36
|
+
flags = build_config.build_settings['OTHER_SWIFT_FLAGS'] || '$(inherited)'
|
|
37
|
+
unless flags.include?('IsolatedConformances')
|
|
38
|
+
build_config.build_settings['OTHER_SWIFT_FLAGS'] = "#{flags} -enable-upcoming-feature IsolatedConformances"
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
`
|
|
44
|
+
const updated = podfile.replace(
|
|
45
|
+
/(post_install do \|installer\|.*?)(^\s+end\s*\nend)/ms,
|
|
46
|
+
`$1${workaround}$2`
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
if (updated !== podfile) {
|
|
50
|
+
fs.writeFileSync(podfilePath, updated)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return config
|
|
54
|
+
},
|
|
55
|
+
])
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
module.exports = withExpoModulesCoreSwift6Fix
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { withDangerousMod } = require('@expo/config-plugins')
|
|
2
|
+
const { mergeContents } = require('@expo/config-plugins/build/utils/generateCode')
|
|
3
|
+
const fs = require('node:fs')
|
|
4
|
+
const path = require('node:path')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* expo config plugin to add pre_install hook for op-sqlite static framework compilation
|
|
8
|
+
* required when using useFrameworks: 'static'
|
|
9
|
+
* based on: https://github.com/OP-Engineering/op-sqlite/issues/238
|
|
10
|
+
*/
|
|
11
|
+
function withOpSqliteStatic(config) {
|
|
12
|
+
return withDangerousMod(config, [
|
|
13
|
+
'ios',
|
|
14
|
+
async (config) => {
|
|
15
|
+
const podfilePath = path.join(config.modRequest.platformProjectRoot, 'Podfile')
|
|
16
|
+
|
|
17
|
+
if (!fs.existsSync(podfilePath)) {
|
|
18
|
+
return config
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const podfileContent = fs.readFileSync(podfilePath, 'utf-8')
|
|
22
|
+
|
|
23
|
+
const setOpSqliteStatic = mergeContents({
|
|
24
|
+
tag: 'op-sqlite-static-library',
|
|
25
|
+
src: podfileContent,
|
|
26
|
+
newSrc: `pre_install do |installer|
|
|
27
|
+
installer.pod_targets.each do |pod|
|
|
28
|
+
if pod.name.eql?('op-sqlite')
|
|
29
|
+
def pod.build_type
|
|
30
|
+
Pod::BuildType.static_library
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end`,
|
|
35
|
+
anchor: /platform :ios.*/,
|
|
36
|
+
offset: 1,
|
|
37
|
+
comment: '#',
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
if (!setOpSqliteStatic.didMerge) {
|
|
41
|
+
return config
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
fs.writeFileSync(podfilePath, setOpSqliteStatic.contents)
|
|
45
|
+
|
|
46
|
+
return config
|
|
47
|
+
},
|
|
48
|
+
])
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = withOpSqliteStatic
|