@o/cli 0.16.18 → 0.16.19-canary.1790191946359
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 +29 -5
- 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 +69 -0
- package/src/tko/commands/run.ts +135 -0
- package/src/tko/commands/script.ts +431 -0
- package/src/tko/commands/skills.ts +288 -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,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skills command group - manage repository agent skills
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
existsSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
readdirSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readlinkSync,
|
|
12
|
+
rmSync,
|
|
13
|
+
rmdirSync,
|
|
14
|
+
symlinkSync,
|
|
15
|
+
unlinkSync,
|
|
16
|
+
} from 'node:fs'
|
|
17
|
+
import { join, relative, resolve } from 'node:path'
|
|
18
|
+
|
|
19
|
+
import { defineCommand } from 'citty'
|
|
20
|
+
import pc from 'picocolors'
|
|
21
|
+
|
|
22
|
+
// --- doc skills generation ---
|
|
23
|
+
|
|
24
|
+
const SKILL_PREFIX = 'takeout-'
|
|
25
|
+
|
|
26
|
+
function getSkillsDirs(cwd: string): string[] {
|
|
27
|
+
return [join(cwd, '.claude', 'skills'), join(cwd, '.agents', 'skills')]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function hasSkillFrontmatter(content: string): boolean {
|
|
31
|
+
if (!content.startsWith('---')) return false
|
|
32
|
+
const endIndex = content.indexOf('---', 3)
|
|
33
|
+
if (endIndex === -1) return false
|
|
34
|
+
const frontmatter = content.slice(3, endIndex)
|
|
35
|
+
return frontmatter.includes('name:') && frontmatter.includes('description:')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function isDevOnly(content: string): boolean {
|
|
39
|
+
if (!content.startsWith('---')) return false
|
|
40
|
+
const endIndex = content.indexOf('---', 3)
|
|
41
|
+
if (endIndex === -1) return false
|
|
42
|
+
const frontmatter = content.slice(3, endIndex)
|
|
43
|
+
return /\bdev:\s*true\b/.test(frontmatter)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// prefer a project's ./skills dir if it exists, else fall back to ./docs. lets
|
|
47
|
+
// consumers cleanly separate agent skills (frontmatter md) from plain docs.
|
|
48
|
+
function resolveLocalDocsDir(cwd: string): string {
|
|
49
|
+
const skillsDir = join(cwd, 'skills')
|
|
50
|
+
if (existsSync(skillsDir)) return skillsDir
|
|
51
|
+
return join(cwd, 'docs')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function collectAllDocs(cwd: string): Array<{ name: string; path: string }> {
|
|
55
|
+
const docs: Array<{ name: string; path: string }> = []
|
|
56
|
+
|
|
57
|
+
const localDocsDir = resolveLocalDocsDir(cwd)
|
|
58
|
+
if (existsSync(localDocsDir)) {
|
|
59
|
+
const files = readdirSync(localDocsDir).filter((f) => f.endsWith('.md'))
|
|
60
|
+
for (const file of files) {
|
|
61
|
+
const name = file.replace(/\.md$/, '')
|
|
62
|
+
docs.push({ name, path: join(localDocsDir, file) })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return docs
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function generateDocSkills(
|
|
70
|
+
cwd: string,
|
|
71
|
+
clean: boolean
|
|
72
|
+
): Promise<{
|
|
73
|
+
symlinked: number
|
|
74
|
+
generated: number
|
|
75
|
+
unchanged: number
|
|
76
|
+
removed: number
|
|
77
|
+
skipped: number
|
|
78
|
+
}> {
|
|
79
|
+
const skillsDirs = getSkillsDirs(cwd)
|
|
80
|
+
const docs = collectAllDocs(cwd)
|
|
81
|
+
const localDocsDir = resolveLocalDocsDir(cwd)
|
|
82
|
+
const expectedSkillNames = new Set<string>()
|
|
83
|
+
|
|
84
|
+
if (docs.length === 0) {
|
|
85
|
+
console.info(pc.yellow('no documentation files found'))
|
|
86
|
+
} else {
|
|
87
|
+
console.info(pc.dim(`found ${docs.length} documentation files`))
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const skillsDir of skillsDirs) {
|
|
91
|
+
if (clean && existsSync(skillsDir)) {
|
|
92
|
+
const existing = readdirSync(skillsDir)
|
|
93
|
+
for (const dir of existing) {
|
|
94
|
+
if (dir.startsWith(SKILL_PREFIX)) {
|
|
95
|
+
rmSync(join(skillsDir, dir), { recursive: true })
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!existsSync(skillsDir)) {
|
|
101
|
+
mkdirSync(skillsDir, { recursive: true })
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let symlinked = 0
|
|
106
|
+
let generated = 0
|
|
107
|
+
let unchanged = 0
|
|
108
|
+
let removed = 0
|
|
109
|
+
let skipped = 0
|
|
110
|
+
const isDev = !!process.env.IS_TAMAGUI_DEV
|
|
111
|
+
|
|
112
|
+
for (const doc of docs) {
|
|
113
|
+
const content = readFileSync(doc.path, 'utf-8')
|
|
114
|
+
if (isDevOnly(content) && !isDev) continue
|
|
115
|
+
|
|
116
|
+
const hasFrontmatter = hasSkillFrontmatter(content)
|
|
117
|
+
|
|
118
|
+
if (hasFrontmatter) {
|
|
119
|
+
const nameMatch = content.match(/^---\s*\nname:\s*([^\n]+)/m)
|
|
120
|
+
if (!nameMatch) continue
|
|
121
|
+
|
|
122
|
+
const skillName = nameMatch[1]!.trim()
|
|
123
|
+
expectedSkillNames.add(skillName)
|
|
124
|
+
|
|
125
|
+
for (const skillsDir of skillsDirs) {
|
|
126
|
+
const skillDir = join(skillsDir, skillName)
|
|
127
|
+
const skillFile = join(skillDir, 'SKILL.md')
|
|
128
|
+
|
|
129
|
+
if (!existsSync(skillDir)) {
|
|
130
|
+
mkdirSync(skillDir, { recursive: true })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const relativePath = relative(skillDir, doc.path)
|
|
134
|
+
|
|
135
|
+
let shouldCreate = true
|
|
136
|
+
try {
|
|
137
|
+
const stat = lstatSync(skillFile)
|
|
138
|
+
if (stat.isSymbolicLink() && existsSync(skillFile)) {
|
|
139
|
+
const existingContent = readFileSync(skillFile, 'utf-8')
|
|
140
|
+
if (existingContent === content) {
|
|
141
|
+
unchanged++
|
|
142
|
+
shouldCreate = false
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (shouldCreate) {
|
|
146
|
+
unlinkSync(skillFile)
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// nothing exists
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!shouldCreate) continue
|
|
153
|
+
|
|
154
|
+
symlinkSync(relativePath, skillFile)
|
|
155
|
+
symlinked++
|
|
156
|
+
|
|
157
|
+
console.info(
|
|
158
|
+
` ${pc.green('⟷')} ${skillName} ${pc.dim(`(${relative(cwd, skillsDir)})`)}`
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
if (!hasFrontmatter) {
|
|
163
|
+
skipped++
|
|
164
|
+
console.info(
|
|
165
|
+
` ${pc.yellow('!')} skipped ${pc.dim(doc.name)} ${pc.dim('(missing skill frontmatter)')}`
|
|
166
|
+
)
|
|
167
|
+
continue
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
for (const skillsDir of skillsDirs) {
|
|
173
|
+
for (const dir of readdirSync(skillsDir)) {
|
|
174
|
+
if (expectedSkillNames.has(dir)) continue
|
|
175
|
+
|
|
176
|
+
const skillDir = join(skillsDir, dir)
|
|
177
|
+
const skillFile = join(skillDir, 'SKILL.md')
|
|
178
|
+
|
|
179
|
+
if (dir.startsWith(SKILL_PREFIX)) {
|
|
180
|
+
rmSync(skillDir, { recursive: true, force: true })
|
|
181
|
+
removed++
|
|
182
|
+
console.info(
|
|
183
|
+
` ${pc.red('✕')} ${dir} ${pc.dim('(removed stale generated skill)')}`
|
|
184
|
+
)
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let shouldUnlink = false
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const stat = lstatSync(skillFile)
|
|
192
|
+
if (stat.isSymbolicLink()) {
|
|
193
|
+
const linkTarget = readlinkSync(skillFile)
|
|
194
|
+
const resolvedTarget = resolve(skillDir, linkTarget)
|
|
195
|
+
shouldUnlink = resolvedTarget.startsWith(`${localDocsDir}/`)
|
|
196
|
+
}
|
|
197
|
+
} catch {
|
|
198
|
+
// ignore unrelated skill directories
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!shouldUnlink) continue
|
|
202
|
+
|
|
203
|
+
unlinkSync(skillFile)
|
|
204
|
+
if (readdirSync(skillDir).length === 0) {
|
|
205
|
+
rmdirSync(skillDir)
|
|
206
|
+
}
|
|
207
|
+
removed++
|
|
208
|
+
console.info(` ${pc.red('✕')} ${dir} ${pc.dim('(removed stale symlink)')}`)
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { symlinked, generated, unchanged, removed, skipped }
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// --- commands ---
|
|
216
|
+
|
|
217
|
+
const generateCommand = defineCommand({
|
|
218
|
+
meta: {
|
|
219
|
+
name: 'generate',
|
|
220
|
+
description: 'Generate Claude Code and Codex skills from documentation',
|
|
221
|
+
},
|
|
222
|
+
args: {
|
|
223
|
+
clean: {
|
|
224
|
+
type: 'boolean',
|
|
225
|
+
description: 'Remove existing takeout-* skills before generating',
|
|
226
|
+
default: false,
|
|
227
|
+
},
|
|
228
|
+
'skip-internal-docs': {
|
|
229
|
+
type: 'boolean',
|
|
230
|
+
description: 'Skip generating skills from internal documentation files',
|
|
231
|
+
default: false,
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
async run({ args }) {
|
|
235
|
+
const cwd = process.cwd()
|
|
236
|
+
const skillsDirs = getSkillsDirs(cwd)
|
|
237
|
+
|
|
238
|
+
console.info()
|
|
239
|
+
console.info(pc.bold(pc.cyan('Generate all skills')))
|
|
240
|
+
console.info()
|
|
241
|
+
|
|
242
|
+
let symlinked = 0
|
|
243
|
+
let generated = 0
|
|
244
|
+
let unchanged = 0
|
|
245
|
+
let removed = 0
|
|
246
|
+
let skipped = 0
|
|
247
|
+
|
|
248
|
+
// 1. doc skills (unless skipped)
|
|
249
|
+
if (!args['skip-internal-docs']) {
|
|
250
|
+
const docStats = await generateDocSkills(cwd, args.clean)
|
|
251
|
+
symlinked = docStats.symlinked
|
|
252
|
+
generated = docStats.generated
|
|
253
|
+
unchanged = docStats.unchanged
|
|
254
|
+
removed = docStats.removed
|
|
255
|
+
skipped = docStats.skipped
|
|
256
|
+
console.info()
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// summary
|
|
260
|
+
console.info()
|
|
261
|
+
console.info(pc.bold('summary:'))
|
|
262
|
+
if (symlinked > 0) console.info(` ${pc.green(`${symlinked} symlinked`)}`)
|
|
263
|
+
if (generated > 0)
|
|
264
|
+
console.info(
|
|
265
|
+
` ${pc.yellow(`${generated} generated`)} ${pc.dim('(add frontmatter to enable symlink)')}`
|
|
266
|
+
)
|
|
267
|
+
if (skipped > 0)
|
|
268
|
+
console.info(
|
|
269
|
+
` ${pc.yellow(`${skipped} skipped`)} ${pc.dim('(missing skill frontmatter)')}`
|
|
270
|
+
)
|
|
271
|
+
if (unchanged > 0) console.info(` ${pc.dim(`${unchanged} unchanged`)}`)
|
|
272
|
+
if (removed > 0) console.info(` ${pc.red(`${removed} removed`)}`)
|
|
273
|
+
for (const skillsDir of skillsDirs) {
|
|
274
|
+
console.info(pc.dim(` skills in ${skillsDir}`))
|
|
275
|
+
}
|
|
276
|
+
console.info()
|
|
277
|
+
},
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
export const skillsCommand = defineCommand({
|
|
281
|
+
meta: {
|
|
282
|
+
name: 'skills',
|
|
283
|
+
description: 'Manage repository agent skills',
|
|
284
|
+
},
|
|
285
|
+
subCommands: {
|
|
286
|
+
generate: generateCommand,
|
|
287
|
+
},
|
|
288
|
+
})
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sync command - sync fork with upstream Takeout repository
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { execSync, spawn, spawnSync } from 'node:child_process'
|
|
6
|
+
import { existsSync, mkdtempSync, readFileSync, writeFileSync } from 'node:fs'
|
|
7
|
+
import { tmpdir } from 'node:os'
|
|
8
|
+
import { join } from 'node:path'
|
|
9
|
+
|
|
10
|
+
import { defineCommand } from 'citty'
|
|
11
|
+
import pc from 'picocolors'
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
confirmContinue,
|
|
15
|
+
promptSelect,
|
|
16
|
+
showError,
|
|
17
|
+
showInfo,
|
|
18
|
+
showStep,
|
|
19
|
+
showSuccess,
|
|
20
|
+
} from '../utils/prompts'
|
|
21
|
+
|
|
22
|
+
const UPSTREAM_REPO = 'tamagui/takeout2'
|
|
23
|
+
const UPSTREAM_REMOTE = 'takeout-upstream'
|
|
24
|
+
const TAKEOUT_FILE = '.takeout'
|
|
25
|
+
|
|
26
|
+
function getSyncPrompt(): string {
|
|
27
|
+
const promptPath = join(process.cwd(), 'docs', 'upgrade.md')
|
|
28
|
+
if (!existsSync(promptPath)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
'Missing docs/upgrade.md. Copy it from https://github.com/tamagui/takeout2/blob/main/docs/upgrade.md and rerun `bun tko sync`.'
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return readFileSync(promptPath, 'utf-8')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function checkToolAvailable(command: string): boolean {
|
|
38
|
+
try {
|
|
39
|
+
// use 'where' on windows, 'which' on unix
|
|
40
|
+
const checkCmd = process.platform === 'win32' ? 'where' : 'which'
|
|
41
|
+
const result = spawnSync(checkCmd, [command])
|
|
42
|
+
return result.status === 0
|
|
43
|
+
} catch {
|
|
44
|
+
return false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ensureUpstreamRemote(): boolean {
|
|
49
|
+
try {
|
|
50
|
+
// check if remote exists
|
|
51
|
+
const remotes = execSync('git remote', { encoding: 'utf-8' })
|
|
52
|
+
if (!remotes.includes(UPSTREAM_REMOTE)) {
|
|
53
|
+
execSync(`git remote add ${UPSTREAM_REMOTE} git@github.com:${UPSTREAM_REPO}.git`, {
|
|
54
|
+
stdio: 'pipe',
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
execSync(`git fetch ${UPSTREAM_REMOTE} --quiet`, { stdio: 'pipe' })
|
|
58
|
+
return true
|
|
59
|
+
} catch {
|
|
60
|
+
return false
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function getUpstreamHeadSha(): string | null {
|
|
65
|
+
try {
|
|
66
|
+
const sha = execSync(`git rev-parse ${UPSTREAM_REMOTE}/main`, { encoding: 'utf-8' })
|
|
67
|
+
return sha.trim()
|
|
68
|
+
} catch {
|
|
69
|
+
return null
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeTakeoutConfig(sha: string): void {
|
|
74
|
+
const configPath = join(process.cwd(), TAKEOUT_FILE)
|
|
75
|
+
const date = new Date().toISOString().split('T')[0]
|
|
76
|
+
const content = `# takeout sync tracking file
|
|
77
|
+
# this file tracks the last synced commit from upstream takeout
|
|
78
|
+
sha=${sha}
|
|
79
|
+
date=${date}
|
|
80
|
+
`
|
|
81
|
+
writeFileSync(configPath, content)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const syncCommand = defineCommand({
|
|
85
|
+
meta: {
|
|
86
|
+
name: 'sync',
|
|
87
|
+
description: 'Sync your fork with the latest Takeout repository',
|
|
88
|
+
},
|
|
89
|
+
args: {
|
|
90
|
+
auto: {
|
|
91
|
+
type: 'boolean',
|
|
92
|
+
description: 'Auto-run with claude-code without prompts (for non-TTY environments)',
|
|
93
|
+
default: false,
|
|
94
|
+
},
|
|
95
|
+
print: {
|
|
96
|
+
type: 'boolean',
|
|
97
|
+
description: 'Print the sync prompt and exit',
|
|
98
|
+
default: false,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
async run({ args }) {
|
|
102
|
+
const isAuto = args.auto
|
|
103
|
+
const isPrint = args.print
|
|
104
|
+
showStep('Takeout Repository Sync')
|
|
105
|
+
console.info()
|
|
106
|
+
|
|
107
|
+
if (!isAuto && !isPrint) {
|
|
108
|
+
showInfo('Takeout sync uses AI to intelligently merge upstream changes')
|
|
109
|
+
console.info()
|
|
110
|
+
console.info(pc.gray('How it works:'))
|
|
111
|
+
console.info(pc.gray(' • Analyzes commits from upstream Takeout repository'))
|
|
112
|
+
console.info(pc.gray(' • Determines which changes are relevant to your fork'))
|
|
113
|
+
console.info(pc.gray(' • Applies changes while preserving your customizations'))
|
|
114
|
+
console.info(pc.gray(' • Handles package ejection automatically'))
|
|
115
|
+
console.info(pc.gray(' • Asks for your input when decisions are needed'))
|
|
116
|
+
console.info()
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// check what tools are available
|
|
120
|
+
const hasClaudeCode = checkToolAvailable('claude')
|
|
121
|
+
const hasCursor = checkToolAvailable('cursor-agent')
|
|
122
|
+
const hasAider = checkToolAvailable('aider')
|
|
123
|
+
|
|
124
|
+
// handle --auto mode
|
|
125
|
+
let choice: string
|
|
126
|
+
if (isAuto) {
|
|
127
|
+
if (!hasClaudeCode) {
|
|
128
|
+
showError('--auto requires claude CLI to be installed')
|
|
129
|
+
process.exit(1)
|
|
130
|
+
}
|
|
131
|
+
choice = 'claude-code'
|
|
132
|
+
} else if (isPrint) {
|
|
133
|
+
choice = 'show-prompt'
|
|
134
|
+
} else {
|
|
135
|
+
const options: Array<{
|
|
136
|
+
value: string
|
|
137
|
+
label: string
|
|
138
|
+
hint: string
|
|
139
|
+
}> = []
|
|
140
|
+
|
|
141
|
+
if (hasClaudeCode) {
|
|
142
|
+
options.push({
|
|
143
|
+
value: 'claude-code',
|
|
144
|
+
label: 'Claude Code (recommended)',
|
|
145
|
+
hint: 'Run sync automatically with Claude Code CLI',
|
|
146
|
+
})
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (hasCursor) {
|
|
150
|
+
options.push({
|
|
151
|
+
value: 'cursor',
|
|
152
|
+
label: 'Cursor Agent',
|
|
153
|
+
hint: 'Run sync automatically with Cursor CLI',
|
|
154
|
+
})
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (hasAider) {
|
|
158
|
+
options.push({
|
|
159
|
+
value: 'aider',
|
|
160
|
+
label: 'Aider',
|
|
161
|
+
hint: 'Run sync automatically with Aider CLI',
|
|
162
|
+
})
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
options.push({
|
|
166
|
+
value: 'show-prompt',
|
|
167
|
+
label: 'Show prompt (copy & paste manually)',
|
|
168
|
+
hint: 'Display the full prompt to use with any LLM',
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
choice = await promptSelect<string>('How would you like to sync?', options)
|
|
172
|
+
|
|
173
|
+
if (choice === 'cancel') {
|
|
174
|
+
console.info()
|
|
175
|
+
showInfo('Sync cancelled')
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.info()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// fetch upstream to get the target SHA before syncing
|
|
183
|
+
console.info(pc.dim('Fetching upstream repository...'))
|
|
184
|
+
if (!ensureUpstreamRemote()) {
|
|
185
|
+
showError('Failed to fetch upstream repository')
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const upstreamSha = getUpstreamHeadSha()
|
|
190
|
+
if (!upstreamSha) {
|
|
191
|
+
showError('Failed to get upstream HEAD SHA')
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
console.info(pc.dim(`Target SHA: ${upstreamSha.slice(0, 7)}`))
|
|
195
|
+
console.info()
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
const prompt = getSyncPrompt()
|
|
199
|
+
|
|
200
|
+
if (choice === 'show-prompt') {
|
|
201
|
+
console.info(pc.dim('='.repeat(80)))
|
|
202
|
+
console.info(prompt)
|
|
203
|
+
console.info(pc.dim('='.repeat(80)))
|
|
204
|
+
console.info()
|
|
205
|
+
showInfo('Copy the prompt above and paste it into your preferred LLM')
|
|
206
|
+
console.info()
|
|
207
|
+
console.info(pc.gray('Recommended LLMs:'))
|
|
208
|
+
console.info(pc.gray(' • Claude Code (best for complex instructions)'))
|
|
209
|
+
console.info(pc.gray(' • ChatGPT'))
|
|
210
|
+
console.info(pc.gray(' • Cursor'))
|
|
211
|
+
console.info(pc.gray(' • Aider'))
|
|
212
|
+
console.info()
|
|
213
|
+
} else if (choice === 'claude-code') {
|
|
214
|
+
showInfo('Starting Claude Code with sync prompt...')
|
|
215
|
+
console.info()
|
|
216
|
+
|
|
217
|
+
if (!isAuto) {
|
|
218
|
+
console.info(
|
|
219
|
+
pc.dim(
|
|
220
|
+
'Note: Claude Code will run in headless mode and make changes automatically.'
|
|
221
|
+
)
|
|
222
|
+
)
|
|
223
|
+
console.info(pc.dim('You will be asked to confirm important decisions.'))
|
|
224
|
+
console.info()
|
|
225
|
+
|
|
226
|
+
const shouldContinue = await confirmContinue('Continue with Claude Code?', true)
|
|
227
|
+
if (!shouldContinue) {
|
|
228
|
+
showInfo('Sync cancelled')
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// write prompt to temp file to avoid shell escaping issues
|
|
234
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'takeout-sync-'))
|
|
235
|
+
const promptFile = join(tempDir, 'prompt.md')
|
|
236
|
+
writeFileSync(promptFile, prompt)
|
|
237
|
+
|
|
238
|
+
// run claude with prompt from stdin
|
|
239
|
+
const claude = spawn('claude', ['-p', '-'], {
|
|
240
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
claude.stdin?.write(prompt)
|
|
244
|
+
claude.stdin?.end()
|
|
245
|
+
|
|
246
|
+
claude.on('close', (code) => {
|
|
247
|
+
console.info()
|
|
248
|
+
if (code === 0) {
|
|
249
|
+
writeTakeoutConfig(upstreamSha)
|
|
250
|
+
showSuccess('Sync completed successfully!')
|
|
251
|
+
console.info(pc.dim(`Updated .takeout to ${upstreamSha.slice(0, 7)}`))
|
|
252
|
+
} else {
|
|
253
|
+
showError(`Claude Code exited with code ${code}`)
|
|
254
|
+
}
|
|
255
|
+
})
|
|
256
|
+
} else if (choice === 'cursor') {
|
|
257
|
+
showInfo('Starting Cursor Agent with sync prompt...')
|
|
258
|
+
console.info()
|
|
259
|
+
|
|
260
|
+
const shouldContinue = await confirmContinue('Continue with Cursor?', true)
|
|
261
|
+
if (!shouldContinue) {
|
|
262
|
+
showInfo('Sync cancelled')
|
|
263
|
+
return
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// run cursor agent with prompt from stdin
|
|
267
|
+
const cursor = spawn('cursor-agent', ['-p', '-'], {
|
|
268
|
+
stdio: ['pipe', 'inherit', 'inherit'],
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
cursor.stdin?.write(prompt)
|
|
272
|
+
cursor.stdin?.end()
|
|
273
|
+
|
|
274
|
+
cursor.on('close', (code) => {
|
|
275
|
+
console.info()
|
|
276
|
+
if (code === 0) {
|
|
277
|
+
writeTakeoutConfig(upstreamSha)
|
|
278
|
+
showSuccess('Sync completed successfully!')
|
|
279
|
+
console.info(pc.dim(`Updated .takeout to ${upstreamSha.slice(0, 7)}`))
|
|
280
|
+
} else {
|
|
281
|
+
showError(`Cursor Agent exited with code ${code}`)
|
|
282
|
+
}
|
|
283
|
+
})
|
|
284
|
+
} else if (choice === 'aider') {
|
|
285
|
+
showInfo('Starting Aider with sync prompt...')
|
|
286
|
+
console.info()
|
|
287
|
+
|
|
288
|
+
const shouldContinue = await confirmContinue('Continue with Aider?', true)
|
|
289
|
+
if (!shouldContinue) {
|
|
290
|
+
showInfo('Sync cancelled')
|
|
291
|
+
return
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// write prompt to temp file for aider
|
|
295
|
+
const tempDir = mkdtempSync(join(tmpdir(), 'takeout-sync-'))
|
|
296
|
+
const promptFile = join(tempDir, 'prompt.md')
|
|
297
|
+
writeFileSync(promptFile, prompt)
|
|
298
|
+
|
|
299
|
+
// run aider with message from file
|
|
300
|
+
const aider = spawn('aider', ['--message-file', promptFile, '--no-stream'], {
|
|
301
|
+
stdio: 'inherit',
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
aider.on('close', (code) => {
|
|
305
|
+
console.info()
|
|
306
|
+
if (code === 0) {
|
|
307
|
+
writeTakeoutConfig(upstreamSha)
|
|
308
|
+
showSuccess('Sync completed successfully!')
|
|
309
|
+
console.info(pc.dim(`Updated .takeout to ${upstreamSha.slice(0, 7)}`))
|
|
310
|
+
} else {
|
|
311
|
+
showError(`Aider exited with code ${code}`)
|
|
312
|
+
}
|
|
313
|
+
})
|
|
314
|
+
}
|
|
315
|
+
} catch (error) {
|
|
316
|
+
showError(error instanceof Error ? error.message : 'Unknown error')
|
|
317
|
+
}
|
|
318
|
+
},
|
|
319
|
+
})
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ASCII art constants for Takeout branding
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
// TAKEOUT in block letters with chinese accent line
|
|
6
|
+
// chinese chars: 麵(noodles) 碼(code) 飯(rice)
|
|
7
|
+
export const TAKEOUT_ASCII = `
|
|
8
|
+
████████╗ █████╗ ██╗ ██╗███████╗ ██████╗ ██╗ ██╗████████╗
|
|
9
|
+
╚══██╔══╝██╔══██╗██║ ██╔╝██╔════╝██╔═══██╗██║ ██║╚══██╔══╝
|
|
10
|
+
██║ ███████║█████╔╝ █████╗ ██║ ██║██║ ██║ ██║
|
|
11
|
+
██║ ██╔══██║██╔═██╗ ██╔══╝ ██║ ██║██║ ██║ ██║
|
|
12
|
+
██║ ██║ ██║██║ ██╗███████╗╚██████╔╝╚██████╔╝ ██║
|
|
13
|
+
╚═╝ ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝
|
|
14
|
+
麵 碼 飯
|
|
15
|
+
`.trim()
|
|
16
|
+
|
|
17
|
+
export const WELCOME_BANNER = TAKEOUT_ASCII
|
package/src/tko/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { cmd } from '@o/scripts/cmd'
|
package/src/tko/types.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export interface PrerequisiteCheck {
|
|
2
|
+
name: string
|
|
3
|
+
required: boolean
|
|
4
|
+
installed: boolean
|
|
5
|
+
version?: string
|
|
6
|
+
requiredVersion?: string
|
|
7
|
+
message?: string
|
|
8
|
+
installUrl?: string
|
|
9
|
+
recommendation?: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface PortCheck {
|
|
13
|
+
port: number
|
|
14
|
+
name: string
|
|
15
|
+
inUse: boolean
|
|
16
|
+
pid?: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ProjectIdentity {
|
|
20
|
+
name: string
|
|
21
|
+
bundleId: string
|
|
22
|
+
domain: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface OnboardingConfig {
|
|
26
|
+
skipPrerequisites?: boolean
|
|
27
|
+
skipServices?: boolean
|
|
28
|
+
skipMigrations?: boolean
|
|
29
|
+
autoStart?: boolean
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface EnvironmentSetup {
|
|
33
|
+
authSecret: string
|
|
34
|
+
githubClientId?: string
|
|
35
|
+
githubClientSecret?: string
|
|
36
|
+
domain: string
|
|
37
|
+
serverUrl: string
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface EnvVariable {
|
|
41
|
+
key: string
|
|
42
|
+
label: string
|
|
43
|
+
description: string
|
|
44
|
+
instructions: string
|
|
45
|
+
required: boolean
|
|
46
|
+
type: 'text' | 'secret' | 'multiline'
|
|
47
|
+
default?: string
|
|
48
|
+
placeholder?: string
|
|
49
|
+
generator?: () => string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface EnvCategory {
|
|
53
|
+
id: string
|
|
54
|
+
name: string
|
|
55
|
+
description: string
|
|
56
|
+
required: boolean
|
|
57
|
+
setupTime?: string
|
|
58
|
+
variables: EnvVariable[]
|
|
59
|
+
}
|