@take-out/cli 0.1.45 → 0.1.46

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.
@@ -8,211 +8,19 @@ import {
8
8
  mkdirSync,
9
9
  readdirSync,
10
10
  readFileSync,
11
+ readlinkSync,
11
12
  rmSync,
13
+ rmdirSync,
12
14
  symlinkSync,
13
15
  unlinkSync,
14
16
  writeFileSync,
15
17
  } from 'node:fs'
16
18
  import { createRequire } from 'node:module'
17
- import { dirname, join, relative } from 'node:path'
18
- import { fileURLToPath } from 'node:url'
19
+ import { dirname, join, relative, resolve } from 'node:path'
19
20
 
20
21
  import { defineCommand } from 'citty'
21
22
  import pc from 'picocolors'
22
23
 
23
- import {
24
- type ScriptMetadata,
25
- discoverScripts,
26
- getAllScriptMetadata,
27
- getLocalScriptsDir,
28
- } from '../utils/script-utils'
29
-
30
- // --- shared helpers ---
31
-
32
- const BUILTIN_COMMANDS: Array<{ name: string; description: string }> = [
33
- { name: 'onboard', description: 'setup wizard for new projects' },
34
- { name: 'docs', description: 'view documentation' },
35
- { name: 'env:setup', description: 'setup environment variables' },
36
- { name: 'run', description: 'run scripts in parallel' },
37
- { name: 'script', description: 'manage and run scripts' },
38
- { name: 'sync', description: 'sync fork with upstream takeout' },
39
- { name: 'changed', description: 'show changes since last sync' },
40
- { name: 'skills', description: 'manage claude code skills' },
41
- { name: 'completion', description: 'shell completion setup' },
42
- ]
43
-
44
- function findScriptsPackageRoot(): string | null {
45
- try {
46
- const resolved = import.meta.resolve('@take-out/scripts/package.json')
47
- const packageJsonPath = fileURLToPath(new URL(resolved))
48
- return join(packageJsonPath, '..', 'src')
49
- } catch {
50
- return null
51
- }
52
- }
53
-
54
- // --- summary skill generation ---
55
-
56
- function buildSummaryDescription(
57
- localScripts: Map<string, string>,
58
- builtInScripts: Map<string, string>
59
- ): string {
60
- const categories = new Set<string>()
61
- const keywords = new Set<string>()
62
-
63
- for (const [name] of [...localScripts, ...builtInScripts]) {
64
- keywords.add(name)
65
- if (name.includes('/')) {
66
- categories.add(name.split('/')[0]!)
67
- }
68
- }
69
-
70
- for (const cmd of BUILTIN_COMMANDS) {
71
- keywords.add(cmd.name)
72
- }
73
-
74
- const categoryList = [...categories].sort().join(', ')
75
-
76
- return (
77
- `CLI scripts and commands reference for the tko (takeout) CLI. ` +
78
- `Use when the user asks to run scripts, manage the project, or needs to know what commands are available. ` +
79
- `tko, takeout, CLI, scripts, commands, bun tko, project tasks, automation, ` +
80
- `${categoryList}, ${[...keywords].sort().join(', ')}`
81
- ).slice(0, 2048)
82
- }
83
-
84
- function buildSummaryContent(
85
- localScripts: Map<string, string>,
86
- builtInScripts: Map<string, string>,
87
- metadata: Map<string, ScriptMetadata>
88
- ): string {
89
- const description = buildSummaryDescription(localScripts, builtInScripts)
90
-
91
- const lines: string[] = []
92
- lines.push('---')
93
- lines.push('name: tko-scripts')
94
- lines.push(`description: ${description}`)
95
- lines.push('---')
96
- lines.push('')
97
- lines.push('# tko CLI - scripts & commands')
98
- lines.push('')
99
- lines.push('run with `bun tko <command>` or `bun tko <script-name>`.')
100
- lines.push('')
101
-
102
- // built-in commands
103
- lines.push('## built-in commands')
104
- lines.push('')
105
- for (const cmd of BUILTIN_COMMANDS) {
106
- lines.push(` ${cmd.name} - ${cmd.description}`)
107
- }
108
- lines.push('')
109
-
110
- // helper to group and format scripts
111
- const formatSection = (title: string, scripts: Map<string, string>) => {
112
- if (scripts.size === 0) return
113
-
114
- const categories = new Map<string, Array<string>>()
115
- const rootScripts: string[] = []
116
-
117
- for (const [name] of scripts) {
118
- if (name.includes('/')) {
119
- const category = name.split('/')[0]!
120
- if (!categories.has(category)) {
121
- categories.set(category, [])
122
- }
123
- categories.get(category)!.push(name)
124
- } else {
125
- rootScripts.push(name)
126
- }
127
- }
128
-
129
- lines.push(`## ${title}`)
130
- lines.push('')
131
-
132
- for (const name of rootScripts) {
133
- const meta = metadata.get(name)
134
- const desc = meta?.description ? ` - ${meta.description}` : ''
135
- const args = meta?.args?.length ? ` [${meta.args.join(', ')}]` : ''
136
- lines.push(` ${name}${desc}${args}`)
137
- }
138
-
139
- for (const [category, categoryScripts] of categories) {
140
- lines.push('')
141
- lines.push(` ${category}/`)
142
- for (const name of categoryScripts) {
143
- const shortName = name.substring(category.length + 1)
144
- const meta = metadata.get(name)
145
- const desc = meta?.description ? ` - ${meta.description}` : ''
146
- const args = meta?.args?.length ? ` [${meta.args.join(', ')}]` : ''
147
- lines.push(` ${shortName}${desc}${args}`)
148
- }
149
- }
150
-
151
- lines.push('')
152
- }
153
-
154
- formatSection('local scripts', localScripts)
155
- formatSection('built-in scripts', builtInScripts)
156
-
157
- // usage
158
- lines.push('## usage')
159
- lines.push('')
160
- lines.push('```bash')
161
- lines.push('bun tko <command> # run a built-in command')
162
- lines.push('bun tko <script-name> # execute direct script')
163
- lines.push(
164
- 'bun tko <group> <script> # execute nested script (e.g. bun tko aws health)'
165
- )
166
- lines.push('bun tko run s1 s2 s3 # run multiple scripts in parallel')
167
- lines.push('bun tko script new <path> # create a new script')
168
- lines.push('```')
169
- lines.push('')
170
-
171
- return lines.join('\n')
172
- }
173
-
174
- async function generateSummary(cwd: string): Promise<boolean> {
175
- const skillsDir = join(cwd, '.claude', 'skills')
176
- const skillName = 'tko-scripts'
177
- const skillDir = join(skillsDir, skillName)
178
- const skillFile = join(skillDir, 'SKILL.md')
179
-
180
- // discover all scripts
181
- const localScripts = discoverScripts(getLocalScriptsDir())
182
- const builtInDir = findScriptsPackageRoot()
183
- const builtInScripts = builtInDir ? discoverScripts(builtInDir) : new Map()
184
-
185
- const allScripts = new Map([...localScripts, ...builtInScripts])
186
- const metadata = await getAllScriptMetadata(allScripts)
187
-
188
- const totalScripts = localScripts.size + builtInScripts.size
189
- console.info(
190
- pc.dim(
191
- `found ${totalScripts} scripts (${localScripts.size} local, ${builtInScripts.size} built-in) + ${BUILTIN_COMMANDS.length} commands`
192
- )
193
- )
194
-
195
- const content = buildSummaryContent(localScripts, builtInScripts, metadata)
196
-
197
- // check if unchanged
198
- try {
199
- const existing = readFileSync(skillFile, 'utf-8')
200
- if (existing === content) {
201
- console.info(` ${pc.dim('tko-scripts')} ${pc.dim('unchanged')}`)
202
- return false
203
- }
204
- } catch {
205
- // doesn't exist yet
206
- }
207
-
208
- if (!existsSync(skillDir)) {
209
- mkdirSync(skillDir, { recursive: true })
210
- }
211
- writeFileSync(skillFile, content)
212
- console.info(` ${pc.green('✓')} tko-scripts`)
213
- return true
214
- }
215
-
216
24
  // --- doc skills generation ---
217
25
 
218
26
  const require = createRequire(import.meta.url)
@@ -312,17 +120,18 @@ function collectAllDocs(
312
120
  async function generateDocSkills(
313
121
  cwd: string,
314
122
  clean: boolean
315
- ): Promise<{ symlinked: number; generated: number; unchanged: number }> {
123
+ ): Promise<{ symlinked: number; generated: number; unchanged: number; removed: number }> {
316
124
  const skillsDir = join(cwd, '.claude', 'skills')
317
125
  const docs = collectAllDocs(cwd)
126
+ const localDocsDir = join(cwd, 'docs')
127
+ const expectedSkillNames = new Set<string>()
318
128
 
319
129
  if (docs.length === 0) {
320
130
  console.info(pc.yellow('no documentation files found'))
321
- return { symlinked: 0, generated: 0, unchanged: 0 }
131
+ } else {
132
+ console.info(pc.dim(`found ${docs.length} documentation files`))
322
133
  }
323
134
 
324
- console.info(pc.dim(`found ${docs.length} documentation files`))
325
-
326
135
  if (clean && existsSync(skillsDir)) {
327
136
  const existing = readdirSync(skillsDir)
328
137
  for (const dir of existing) {
@@ -339,6 +148,7 @@ async function generateDocSkills(
339
148
  let symlinked = 0
340
149
  let generated = 0
341
150
  let unchanged = 0
151
+ let removed = 0
342
152
  const isDev = !!process.env.IS_TAMAGUI_DEV
343
153
 
344
154
  for (const doc of docs) {
@@ -352,6 +162,7 @@ async function generateDocSkills(
352
162
  if (!nameMatch) continue
353
163
 
354
164
  const skillName = nameMatch[1]!.trim()
165
+ expectedSkillNames.add(skillName)
355
166
  const skillDir = join(skillsDir, skillName)
356
167
  const skillFile = join(skillDir, 'SKILL.md')
357
168
 
@@ -390,6 +201,7 @@ async function generateDocSkills(
390
201
  } else {
391
202
  const baseName = toSkillName(doc.name)
392
203
  const skillName = `${SKILL_PREFIX}${baseName}`
204
+ expectedSkillNames.add(skillName)
393
205
  const skillDir = join(skillsDir, skillName)
394
206
  const skillFile = join(skillDir, 'SKILL.md')
395
207
 
@@ -438,33 +250,45 @@ ${content}
438
250
  }
439
251
  }
440
252
 
441
- return { symlinked, generated, unchanged }
442
- }
443
-
444
- // --- commands ---
253
+ for (const dir of readdirSync(skillsDir)) {
254
+ if (expectedSkillNames.has(dir)) continue
255
+
256
+ const skillDir = join(skillsDir, dir)
257
+ const skillFile = join(skillDir, 'SKILL.md')
258
+ let shouldUnlink = false
259
+
260
+ try {
261
+ const stat = lstatSync(skillFile)
262
+ if (stat.isSymbolicLink()) {
263
+ const linkTarget = readlinkSync(skillFile)
264
+ const resolvedTarget = resolve(skillDir, linkTarget)
265
+ shouldUnlink =
266
+ resolvedTarget.startsWith(`${localDocsDir}/`) ||
267
+ (!!DOCS_DIR && resolvedTarget.startsWith(`${DOCS_DIR}/`))
268
+ }
269
+ } catch {
270
+ // ignore unrelated skill directories
271
+ }
445
272
 
446
- const scriptsCommand = defineCommand({
447
- meta: {
448
- name: 'scripts',
449
- description: 'Generate a skill summarizing all tko scripts and commands',
450
- },
451
- async run() {
452
- const cwd = process.cwd()
273
+ if (!shouldUnlink) continue
453
274
 
454
- console.info()
455
- console.info(pc.bold(pc.cyan('Generate scripts skill')))
456
- console.info()
275
+ unlinkSync(skillFile)
276
+ if (readdirSync(skillDir).length === 0) {
277
+ rmdirSync(skillDir)
278
+ }
279
+ removed++
280
+ console.info(` ${pc.red('✕')} ${dir} ${pc.dim('(removed stale symlink)')}`)
281
+ }
457
282
 
458
- await generateSummary(cwd)
283
+ return { symlinked, generated, unchanged, removed }
284
+ }
459
285
 
460
- console.info()
461
- },
462
- })
286
+ // --- commands ---
463
287
 
464
288
  const generateCommand = defineCommand({
465
289
  meta: {
466
290
  name: 'generate',
467
- description: 'Generate all Claude Code skills (doc skills + summary)',
291
+ description: 'Generate Claude Code skills from documentation',
468
292
  },
469
293
  args: {
470
294
  clean: {
@@ -489,6 +313,7 @@ const generateCommand = defineCommand({
489
313
  let symlinked = 0
490
314
  let generated = 0
491
315
  let unchanged = 0
316
+ let removed = 0
492
317
 
493
318
  // 1. doc skills (unless skipped)
494
319
  if (!args['skip-internal-docs']) {
@@ -496,12 +321,10 @@ const generateCommand = defineCommand({
496
321
  symlinked = docStats.symlinked
497
322
  generated = docStats.generated
498
323
  unchanged = docStats.unchanged
324
+ removed = docStats.removed
499
325
  console.info()
500
326
  }
501
327
 
502
- // 2. scripts summary skill
503
- await generateSummary(cwd)
504
-
505
328
  // summary
506
329
  console.info()
507
330
  console.info(pc.bold('summary:'))
@@ -511,6 +334,7 @@ const generateCommand = defineCommand({
511
334
  ` ${pc.yellow(`${generated} generated`)} ${pc.dim('(add frontmatter to enable symlink)')}`
512
335
  )
513
336
  if (unchanged > 0) console.info(` ${pc.dim(`${unchanged} unchanged`)}`)
337
+ if (removed > 0) console.info(` ${pc.red(`${removed} removed`)}`)
514
338
  console.info(pc.dim(` skills in ${skillsDir}`))
515
339
  console.info()
516
340
  },
@@ -523,6 +347,5 @@ export const skillsCommand = defineCommand({
523
347
  },
524
348
  subCommands: {
525
349
  generate: generateCommand,
526
- scripts: scriptsCommand,
527
350
  },
528
351
  })
@@ -1 +1 @@
1
- {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/commands/skills.ts"],"names":[],"mappings":"AAAA;;GAEG;AAogBH,eAAO,MAAM,aAAa,qDASxB,CAAA"}
1
+ {"version":3,"file":"skills.d.ts","sourceRoot":"","sources":["../../src/commands/skills.ts"],"names":[],"mappings":"AAAA;;GAEG;AAoVH,eAAO,MAAM,aAAa,qDAQxB,CAAA"}