@take-out/cli 0.1.12 → 0.1.14

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.
Files changed (38) hide show
  1. package/README.md +5 -5
  2. package/dist/cjs/cli.cjs +2 -1
  3. package/dist/cjs/cli.js +2 -0
  4. package/dist/cjs/cli.js.map +1 -1
  5. package/dist/cjs/commands/docs.cjs +1 -167
  6. package/dist/cjs/commands/docs.js +1 -138
  7. package/dist/cjs/commands/docs.js.map +1 -1
  8. package/dist/cjs/commands/skills.cjs +355 -0
  9. package/dist/cjs/commands/skills.js +277 -0
  10. package/dist/cjs/commands/skills.js.map +6 -0
  11. package/dist/cjs/utils/script-listing.cjs +1 -1
  12. package/dist/cjs/utils/script-listing.js +1 -1
  13. package/dist/cjs/utils/script-listing.js.map +1 -1
  14. package/dist/esm/cli.js +2 -0
  15. package/dist/esm/cli.js.map +1 -1
  16. package/dist/esm/cli.mjs +2 -1
  17. package/dist/esm/cli.mjs.map +1 -1
  18. package/dist/esm/commands/docs.js +3 -150
  19. package/dist/esm/commands/docs.js.map +1 -1
  20. package/dist/esm/commands/docs.mjs +3 -169
  21. package/dist/esm/commands/docs.mjs.map +1 -1
  22. package/dist/esm/commands/skills.js +273 -0
  23. package/dist/esm/commands/skills.js.map +6 -0
  24. package/dist/esm/commands/skills.mjs +320 -0
  25. package/dist/esm/commands/skills.mjs.map +1 -0
  26. package/dist/esm/utils/script-listing.js +1 -1
  27. package/dist/esm/utils/script-listing.js.map +1 -1
  28. package/dist/esm/utils/script-listing.mjs +1 -1
  29. package/dist/esm/utils/script-listing.mjs.map +1 -1
  30. package/package.json +4 -4
  31. package/src/cli.ts +2 -0
  32. package/src/commands/docs.ts +2 -288
  33. package/src/commands/skills.ts +528 -0
  34. package/src/utils/script-listing.ts +1 -0
  35. package/types/commands/docs.d.ts.map +1 -1
  36. package/types/commands/skills.d.ts +5 -0
  37. package/types/commands/skills.d.ts.map +1 -0
  38. package/types/utils/script-listing.d.ts.map +1 -1
@@ -0,0 +1,528 @@
1
+ /**
2
+ * skills command group - manage claude code skills
3
+ */
4
+
5
+ import {
6
+ existsSync,
7
+ lstatSync,
8
+ mkdirSync,
9
+ readdirSync,
10
+ readFileSync,
11
+ rmSync,
12
+ symlinkSync,
13
+ unlinkSync,
14
+ writeFileSync,
15
+ } from 'node:fs'
16
+ import { createRequire } from 'node:module'
17
+ import { dirname, join, relative } from 'node:path'
18
+ import { fileURLToPath } from 'node:url'
19
+
20
+ import { defineCommand } from 'citty'
21
+ import pc from 'picocolors'
22
+
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
+ // --- doc skills generation ---
217
+
218
+ const require = createRequire(import.meta.url)
219
+ let DOCS_DIR: string
220
+ try {
221
+ DOCS_DIR = dirname(require.resolve('@take-out/docs/package.json'))
222
+ } catch {
223
+ DOCS_DIR = ''
224
+ }
225
+
226
+ const SKILL_PREFIX = 'takeout-'
227
+
228
+ function hasSkillFrontmatter(content: string): boolean {
229
+ if (!content.startsWith('---')) return false
230
+ const endIndex = content.indexOf('---', 3)
231
+ if (endIndex === -1) return false
232
+ const frontmatter = content.slice(3, endIndex)
233
+ return frontmatter.includes('name:') && frontmatter.includes('description:')
234
+ }
235
+
236
+ function isDevOnly(content: string): boolean {
237
+ if (!content.startsWith('---')) return false
238
+ const endIndex = content.indexOf('---', 3)
239
+ if (endIndex === -1) return false
240
+ const frontmatter = content.slice(3, endIndex)
241
+ return /\bdev:\s*true\b/.test(frontmatter)
242
+ }
243
+
244
+ function extractDocMeta(content: string): { title: string; description: string } {
245
+ const lines = content.split('\n')
246
+ let title = ''
247
+ let description = ''
248
+
249
+ let startLine = 0
250
+ if (lines[0]?.trim() === '---') {
251
+ for (let i = 1; i < lines.length; i++) {
252
+ if (lines[i]?.trim() === '---') {
253
+ startLine = i + 1
254
+ break
255
+ }
256
+ }
257
+ }
258
+
259
+ for (let i = startLine; i < lines.length; i++) {
260
+ const trimmed = lines[i]?.trim() || ''
261
+ if (!title && trimmed.startsWith('# ')) {
262
+ title = trimmed.slice(2).trim()
263
+ continue
264
+ }
265
+ if (title && trimmed && !trimmed.startsWith('#')) {
266
+ description = trimmed
267
+ break
268
+ }
269
+ }
270
+
271
+ return { title, description }
272
+ }
273
+
274
+ function toSkillName(name: string): string {
275
+ return name
276
+ .toLowerCase()
277
+ .replace(/[^a-z0-9-]/g, '-')
278
+ .replace(/-+/g, '-')
279
+ .replace(/^-|-$/g, '')
280
+ .slice(0, 64)
281
+ }
282
+
283
+ function collectAllDocs(
284
+ cwd: string
285
+ ): Array<{ name: string; path: string; source: 'package' | 'local' }> {
286
+ const docs: Array<{ name: string; path: string; source: 'package' | 'local' }> = []
287
+ const seen = new Set<string>()
288
+
289
+ const localDocsDir = join(cwd, 'docs')
290
+ if (existsSync(localDocsDir)) {
291
+ const files = readdirSync(localDocsDir).filter((f) => f.endsWith('.md'))
292
+ for (const file of files) {
293
+ const name = file.replace(/\.md$/, '')
294
+ docs.push({ name, path: join(localDocsDir, file), source: 'local' })
295
+ seen.add(name)
296
+ }
297
+ }
298
+
299
+ if (DOCS_DIR && existsSync(DOCS_DIR)) {
300
+ const files = readdirSync(DOCS_DIR).filter((f) => f.endsWith('.md'))
301
+ for (const file of files) {
302
+ const name = file.replace(/\.md$/, '')
303
+ if (!seen.has(name)) {
304
+ docs.push({ name, path: join(DOCS_DIR, file), source: 'package' })
305
+ }
306
+ }
307
+ }
308
+
309
+ return docs
310
+ }
311
+
312
+ async function generateDocSkills(
313
+ cwd: string,
314
+ clean: boolean
315
+ ): Promise<{ symlinked: number; generated: number; unchanged: number }> {
316
+ const skillsDir = join(cwd, '.claude', 'skills')
317
+ const docs = collectAllDocs(cwd)
318
+
319
+ if (docs.length === 0) {
320
+ console.info(pc.yellow('no documentation files found'))
321
+ return { symlinked: 0, generated: 0, unchanged: 0 }
322
+ }
323
+
324
+ console.info(pc.dim(`found ${docs.length} documentation files`))
325
+
326
+ if (clean && existsSync(skillsDir)) {
327
+ const existing = readdirSync(skillsDir)
328
+ for (const dir of existing) {
329
+ if (dir.startsWith(SKILL_PREFIX)) {
330
+ rmSync(join(skillsDir, dir), { recursive: true })
331
+ }
332
+ }
333
+ }
334
+
335
+ if (!existsSync(skillsDir)) {
336
+ mkdirSync(skillsDir, { recursive: true })
337
+ }
338
+
339
+ let symlinked = 0
340
+ let generated = 0
341
+ let unchanged = 0
342
+ const isDev = !!process.env.IS_TAMAGUI_DEV
343
+
344
+ for (const doc of docs) {
345
+ const content = readFileSync(doc.path, 'utf-8')
346
+ if (isDevOnly(content) && !isDev) continue
347
+
348
+ const hasFrontmatter = hasSkillFrontmatter(content)
349
+
350
+ if (hasFrontmatter) {
351
+ const nameMatch = content.match(/^---\s*\nname:\s*([^\n]+)/m)
352
+ if (!nameMatch) continue
353
+
354
+ const skillName = nameMatch[1]!.trim()
355
+ const skillDir = join(skillsDir, skillName)
356
+ const skillFile = join(skillDir, 'SKILL.md')
357
+
358
+ if (!existsSync(skillDir)) {
359
+ mkdirSync(skillDir, { recursive: true })
360
+ }
361
+
362
+ const relativePath = relative(skillDir, doc.path)
363
+
364
+ let shouldCreate = true
365
+ try {
366
+ const stat = lstatSync(skillFile)
367
+ if (stat.isSymbolicLink() && existsSync(skillFile)) {
368
+ const existingContent = readFileSync(skillFile, 'utf-8')
369
+ if (existingContent === content) {
370
+ unchanged++
371
+ shouldCreate = false
372
+ }
373
+ }
374
+ if (shouldCreate) {
375
+ unlinkSync(skillFile)
376
+ }
377
+ } catch {
378
+ // nothing exists
379
+ }
380
+
381
+ if (!shouldCreate) continue
382
+
383
+ symlinkSync(relativePath, skillFile)
384
+ symlinked++
385
+
386
+ const sourceLabel = doc.source === 'local' ? pc.blue('local') : pc.dim('package')
387
+ console.info(
388
+ ` ${pc.green('⟷')} ${skillName} ${sourceLabel} ${pc.dim('(symlink)')}`
389
+ )
390
+ } else {
391
+ const baseName = toSkillName(doc.name)
392
+ const skillName = `${SKILL_PREFIX}${baseName}`
393
+ const skillDir = join(skillsDir, skillName)
394
+ const skillFile = join(skillDir, 'SKILL.md')
395
+
396
+ if (!existsSync(skillDir)) {
397
+ mkdirSync(skillDir, { recursive: true })
398
+ }
399
+
400
+ const { title, description } = extractDocMeta(content)
401
+ const skillDescription = description
402
+ ? `${title}. ${description}`.slice(0, 1024)
403
+ : title.slice(0, 1024)
404
+
405
+ const skillContent = `---
406
+ name: ${skillName}
407
+ description: ${skillDescription}
408
+ ---
409
+
410
+ ${content}
411
+ `
412
+
413
+ let shouldWrite = true
414
+ try {
415
+ const stat = lstatSync(skillFile)
416
+ if (stat.isSymbolicLink()) {
417
+ unlinkSync(skillFile)
418
+ } else {
419
+ const existing = readFileSync(skillFile, 'utf-8')
420
+ if (existing === skillContent) {
421
+ unchanged++
422
+ shouldWrite = false
423
+ }
424
+ }
425
+ } catch {
426
+ // nothing exists
427
+ }
428
+
429
+ if (!shouldWrite) continue
430
+
431
+ writeFileSync(skillFile, skillContent)
432
+ generated++
433
+
434
+ const sourceLabel = doc.source === 'local' ? pc.blue('local') : pc.dim('package')
435
+ console.info(
436
+ ` ${pc.green('✓')} ${skillName} ${sourceLabel} ${pc.dim('(generated)')}`
437
+ )
438
+ }
439
+ }
440
+
441
+ return { symlinked, generated, unchanged }
442
+ }
443
+
444
+ // --- commands ---
445
+
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()
453
+
454
+ console.info()
455
+ console.info(pc.bold(pc.cyan('Generate scripts skill')))
456
+ console.info()
457
+
458
+ await generateSummary(cwd)
459
+
460
+ console.info()
461
+ },
462
+ })
463
+
464
+ const generateCommand = defineCommand({
465
+ meta: {
466
+ name: 'generate',
467
+ description: 'Generate all Claude Code skills (doc skills + summary)',
468
+ },
469
+ args: {
470
+ clean: {
471
+ type: 'boolean',
472
+ description: 'Remove existing takeout-* skills before generating',
473
+ default: false,
474
+ },
475
+ 'skip-internal-docs': {
476
+ type: 'boolean',
477
+ description: 'Skip generating skills from internal documentation files',
478
+ default: false,
479
+ },
480
+ },
481
+ async run({ args }) {
482
+ const cwd = process.cwd()
483
+ const skillsDir = join(cwd, '.claude', 'skills')
484
+
485
+ console.info()
486
+ console.info(pc.bold(pc.cyan('Generate all skills')))
487
+ console.info()
488
+
489
+ let symlinked = 0
490
+ let generated = 0
491
+ let unchanged = 0
492
+
493
+ // 1. doc skills (unless skipped)
494
+ if (!args['skip-internal-docs']) {
495
+ const docStats = await generateDocSkills(cwd, args.clean)
496
+ symlinked = docStats.symlinked
497
+ generated = docStats.generated
498
+ unchanged = docStats.unchanged
499
+ console.info()
500
+ }
501
+
502
+ // 2. scripts summary skill
503
+ await generateSummary(cwd)
504
+
505
+ // summary
506
+ console.info()
507
+ console.info(pc.bold('summary:'))
508
+ if (symlinked > 0) console.info(` ${pc.green(`${symlinked} symlinked`)}`)
509
+ if (generated > 0)
510
+ console.info(
511
+ ` ${pc.yellow(`${generated} generated`)} ${pc.dim('(add frontmatter to enable symlink)')}`
512
+ )
513
+ if (unchanged > 0) console.info(` ${pc.dim(`${unchanged} unchanged`)}`)
514
+ console.info(pc.dim(` skills in ${skillsDir}`))
515
+ console.info()
516
+ },
517
+ })
518
+
519
+ export const skillsCommand = defineCommand({
520
+ meta: {
521
+ name: 'skills',
522
+ description: 'Manage Claude Code skills',
523
+ },
524
+ subCommands: {
525
+ generate: generateCommand,
526
+ scripts: scriptsCommand,
527
+ },
528
+ })
@@ -101,6 +101,7 @@ export async function listAllScripts(includeCommands = true) {
101
101
  console.info(` ${pc.green('env:setup')} - Setup environment variables`)
102
102
  console.info(` ${pc.green('run')} - Run scripts in parallel`)
103
103
  console.info(` ${pc.green('script')} - Manage and run scripts`)
104
+ console.info(` ${pc.green('skills')} - Manage Claude Code skills`)
104
105
  console.info(` ${pc.green('sync')} - Sync fork with upstream Takeout`)
105
106
  console.info(` ${pc.green('changed')} - Show changes since last sync`)
106
107
  console.info(` ${pc.green('completion')} - Shell completion setup`)
@@ -1 +1 @@
1
- {"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/commands/docs.ts"],"names":[],"mappings":"AAAA;;GAEG;AA4kBH,eAAO,MAAM,WAAW,qDAYtB,CAAA"}
1
+ {"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/commands/docs.ts"],"names":[],"mappings":"AAAA;;GAEG;AA+SH,eAAO,MAAM,WAAW,qDAWtB,CAAA"}
@@ -0,0 +1,5 @@
1
+ /**
2
+ * skills command group - manage claude code skills
3
+ */
4
+ export declare const skillsCommand: import("citty").CommandDef<import("citty").ArgsDef>;
5
+ //# sourceMappingURL=skills.d.ts.map
@@ -0,0 +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 +1 @@
1
- {"version":3,"file":"script-listing.d.ts","sourceRoot":"","sources":["../../src/utils/script-listing.ts"],"names":[],"mappings":"AAAA;;GAEG;AAsFH,wBAAsB,cAAc,CAAC,eAAe,UAAO,iBA0D1D;AAGD,wBAAsB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA2ChF"}
1
+ {"version":3,"file":"script-listing.d.ts","sourceRoot":"","sources":["../../src/utils/script-listing.ts"],"names":[],"mappings":"AAAA;;GAEG;AAsFH,wBAAsB,cAAc,CAAC,eAAe,UAAO,iBA2D1D;AAGD,wBAAsB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA2ChF"}