create-cascade-skill 0.1.6

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 (3) hide show
  1. package/README.md +38 -0
  2. package/index.js +561 -0
  3. package/package.json +22 -0
package/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # create-cascade-skill
2
+
3
+ Install CascadeTUI skills for external coding agents with an interactive CLI.
4
+
5
+ ## Usage
6
+
7
+ ```bash
8
+ npx create-cascade-skill
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - Detects installed agents automatically.
14
+ - Supports multi-select installation in one run.
15
+ - Provides a direct command for all detected agents.
16
+ - Works in interactive and non-interactive modes.
17
+ - Installs global `SKILL.md` files with Agent Skills frontmatter.
18
+
19
+ ## Supported agents
20
+
21
+ - `codex`
22
+ - `claude-code`
23
+ - `cursor`
24
+ - `windsurf`
25
+ - `cline`
26
+ - `roo-code`
27
+ - `continue`
28
+ - `aider`
29
+ - `gemini-cli`
30
+ - `openhands`
31
+ - `goose`
32
+ - `ami`
33
+
34
+ ## CLI options
35
+
36
+ ```bash
37
+ npx create-cascade-skill --help
38
+ ```
package/index.js ADDED
@@ -0,0 +1,561 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { existsSync, mkdirSync, writeFileSync } from "node:fs"
4
+ import { resolve, join } from "node:path"
5
+ import process from "node:process"
6
+ import { spawnSync } from "node:child_process"
7
+ import { createInterface } from "node:readline/promises"
8
+ import { emitKeypressEvents } from "node:readline"
9
+
10
+ const ANSI_RESET = "\x1b[0m"
11
+ const ANSI_BOLD = "\x1b[1m"
12
+ const ANSI_DIM = "\x1b[2m"
13
+ const ANSI_CYAN = "\x1b[36m"
14
+ const ANSI_GREEN = "\x1b[32m"
15
+ const ANSI_YELLOW = "\x1b[33m"
16
+
17
+ function getHomeDirectory() {
18
+ return process.env.HOME || process.env.USERPROFILE || process.cwd()
19
+ }
20
+
21
+ function commandExists(command) {
22
+ const lookup = process.platform === "win32" ? "where" : "which"
23
+ const result = spawnSync(lookup, [command], { stdio: "ignore" })
24
+ return result.status === 0
25
+ }
26
+
27
+ function parseAgentsArg(value) {
28
+ return value
29
+ .split(",")
30
+ .map((part) => part.trim().toLowerCase())
31
+ .filter((part) => part.length > 0)
32
+ }
33
+
34
+ function unique(items) {
35
+ return [...new Set(items)]
36
+ }
37
+
38
+ function getAgents() {
39
+ const home = getHomeDirectory()
40
+ const appData = process.env.APPDATA || join(home, "AppData", "Roaming")
41
+
42
+ return [
43
+ {
44
+ id: "codex",
45
+ label: "OpenAI Codex",
46
+ description: "Install in ~/.codex/skills/cascadetui/SKILL.md",
47
+ commands: ["codex"],
48
+ detectPaths: [join(home, ".codex")],
49
+ installPath: join(home, ".codex", "skills", "cascadetui", "SKILL.md"),
50
+ flavor: "codex",
51
+ },
52
+ {
53
+ id: "claude-code",
54
+ label: "Claude Code",
55
+ description: "Install in ~/.claude/skills/cascadetui/SKILL.md",
56
+ commands: ["claude"],
57
+ detectPaths: [join(home, ".claude")],
58
+ installPath: join(home, ".claude", "skills", "cascadetui", "SKILL.md"),
59
+ flavor: "claude",
60
+ },
61
+ {
62
+ id: "cursor",
63
+ label: "Cursor",
64
+ description: "Install in ~/.cursor/skills/cascadetui/SKILL.md",
65
+ commands: ["cursor"],
66
+ detectPaths: [join(home, ".cursor"), join(appData, "Cursor")],
67
+ installPath: join(home, ".cursor", "skills", "cascadetui", "SKILL.md"),
68
+ flavor: "cursor",
69
+ },
70
+ {
71
+ id: "windsurf",
72
+ label: "Windsurf",
73
+ description: "Install in ~/.codeium/windsurf/skills/cascadetui/SKILL.md",
74
+ commands: ["windsurf"],
75
+ detectPaths: [join(home, ".codeium"), join(appData, "Codeium")],
76
+ installPath: join(home, ".codeium", "windsurf", "skills", "cascadetui", "SKILL.md"),
77
+ flavor: "generic",
78
+ },
79
+ {
80
+ id: "cline",
81
+ label: "Cline",
82
+ description: "Install in ~/.cline/skills/cascadetui/SKILL.md",
83
+ commands: ["cline"],
84
+ detectPaths: [join(home, ".cline")],
85
+ installPath: join(home, ".cline", "skills", "cascadetui", "SKILL.md"),
86
+ flavor: "generic",
87
+ },
88
+ {
89
+ id: "roo-code",
90
+ label: "Roo Code",
91
+ description: "Install in ~/.roo/skills/cascadetui/SKILL.md",
92
+ commands: ["roo", "roo-code"],
93
+ detectPaths: [join(home, ".roo"), join(home, ".roocode")],
94
+ installPath: join(home, ".roo", "skills", "cascadetui", "SKILL.md"),
95
+ flavor: "generic",
96
+ },
97
+ {
98
+ id: "continue",
99
+ label: "Continue",
100
+ description: "Install in ~/.continue/skills/cascadetui/SKILL.md",
101
+ commands: ["continue"],
102
+ detectPaths: [join(home, ".continue")],
103
+ installPath: join(home, ".continue", "skills", "cascadetui", "SKILL.md"),
104
+ flavor: "generic",
105
+ },
106
+ {
107
+ id: "aider",
108
+ label: "Aider",
109
+ description: "Install in ~/.aider/skills/cascadetui/SKILL.md",
110
+ commands: ["aider"],
111
+ detectPaths: [join(home, ".aider.conf.yml"), join(home, ".aider")],
112
+ installPath: join(home, ".aider", "skills", "cascadetui", "SKILL.md"),
113
+ flavor: "generic",
114
+ },
115
+ {
116
+ id: "gemini-cli",
117
+ label: "Gemini CLI",
118
+ description: "Install in ~/.gemini/skills/cascadetui/SKILL.md",
119
+ commands: ["gemini"],
120
+ detectPaths: [join(home, ".gemini")],
121
+ installPath: join(home, ".gemini", "skills", "cascadetui", "SKILL.md"),
122
+ flavor: "generic",
123
+ },
124
+ {
125
+ id: "openhands",
126
+ label: "OpenHands",
127
+ description: "Install in ~/.openhands/skills/cascadetui/SKILL.md",
128
+ commands: ["openhands"],
129
+ detectPaths: [join(home, ".openhands")],
130
+ installPath: join(home, ".openhands", "skills", "cascadetui", "SKILL.md"),
131
+ flavor: "generic",
132
+ },
133
+ {
134
+ id: "goose",
135
+ label: "Goose",
136
+ description: "Install in ~/.config/goose/skills/cascadetui/SKILL.md",
137
+ commands: ["goose"],
138
+ detectPaths: [join(home, ".config", "goose"), join(appData, "goose")],
139
+ installPath: join(home, ".config", "goose", "skills", "cascadetui", "SKILL.md"),
140
+ flavor: "generic",
141
+ },
142
+ {
143
+ id: "ami",
144
+ label: "Ami",
145
+ description: "Install in ~/.ami/skills/cascadetui/SKILL.md",
146
+ commands: ["ami"],
147
+ detectPaths: [join(home, ".ami")],
148
+ installPath: join(home, ".ami", "skills", "cascadetui", "SKILL.md"),
149
+ flavor: "generic",
150
+ },
151
+ ]
152
+ }
153
+
154
+ function parseArgs(argv) {
155
+ const args = argv.slice(2)
156
+ const options = {
157
+ agents: [],
158
+ allDetected: false,
159
+ list: false,
160
+ dryRun: false,
161
+ help: false,
162
+ }
163
+
164
+ for (let i = 0; i < args.length; i += 1) {
165
+ const arg = args[i]
166
+
167
+ if (arg === "--agents" || arg === "-a") {
168
+ options.agents.push(...parseAgentsArg(args[i + 1] || ""))
169
+ i += 1
170
+ continue
171
+ }
172
+ if (arg.startsWith("--agents=")) {
173
+ options.agents.push(...parseAgentsArg(arg.slice("--agents=".length)))
174
+ continue
175
+ }
176
+ if (arg === "--all-detected") {
177
+ options.allDetected = true
178
+ continue
179
+ }
180
+ if (arg === "--list") {
181
+ options.list = true
182
+ continue
183
+ }
184
+ if (arg === "--dry-run") {
185
+ options.dryRun = true
186
+ continue
187
+ }
188
+ if (arg === "--help" || arg === "-h") {
189
+ options.help = true
190
+ continue
191
+ }
192
+ throw new Error(`Unknown argument: ${arg}`)
193
+ }
194
+
195
+ options.agents = unique(options.agents)
196
+ return options
197
+ }
198
+
199
+ function printHelp() {
200
+ console.log("Usage: npx create-cascade-skill [options]")
201
+ console.log("")
202
+ console.log("Options:")
203
+ console.log(" -a, --agents <ids> Comma-separated agent IDs to install")
204
+ console.log(" --all-detected Install for all detected agents")
205
+ console.log(" --list Print supported and detected agents")
206
+ console.log(" --dry-run Preview files without writing")
207
+ console.log(" -h, --help Show help")
208
+ console.log("")
209
+ console.log("Examples:")
210
+ console.log(" npx create-cascade-skill")
211
+ console.log(" npx create-cascade-skill --all-detected")
212
+ console.log(" npx create-cascade-skill --agents codex,cursor,cline")
213
+ console.log(" npx create-cascade-skill --agents codex --dry-run")
214
+ }
215
+
216
+ function detectAgents(agents) {
217
+ return agents.map((agent) => {
218
+ const commandHit = agent.commands.some((command) => commandExists(command))
219
+ const pathHit = agent.detectPaths.some((path) => existsSync(path))
220
+ return { ...agent, detected: commandHit || pathHit }
221
+ })
222
+ }
223
+
224
+ function printList(agents) {
225
+ console.log(`${ANSI_BOLD}Supported agents${ANSI_RESET}`)
226
+ for (const agent of agents) {
227
+ const marker = agent.detected ? `${ANSI_GREEN}detected${ANSI_RESET}` : `${ANSI_YELLOW}not detected${ANSI_RESET}`
228
+ console.log(`- ${agent.id.padEnd(12)} ${marker} ${agent.label}`)
229
+ }
230
+
231
+ const detected = agents.filter((agent) => agent.detected).map((agent) => agent.id)
232
+ console.log("")
233
+ if (detected.length > 0) {
234
+ console.log(`Detected IDs: ${detected.join(", ")}`)
235
+ console.log(`Install all detected: npx create-cascade-skill --agents ${detected.join(",")}`)
236
+ } else {
237
+ console.log("Detected IDs: none")
238
+ }
239
+ }
240
+
241
+ function validateAgentIds(selected, agents) {
242
+ const allowed = new Set(agents.map((agent) => agent.id))
243
+ for (const id of selected) {
244
+ if (!allowed.has(id)) {
245
+ throw new Error(`Unknown agent id: ${id}`)
246
+ }
247
+ }
248
+ }
249
+
250
+ function promptLine(rl, label) {
251
+ return rl.question(label)
252
+ }
253
+
254
+ function toSelectableOptions(agents) {
255
+ return agents.map((agent) => ({
256
+ id: agent.id,
257
+ label: agent.label,
258
+ description: `${agent.description}${agent.detected ? " [detected]" : ""}`,
259
+ }))
260
+ }
261
+
262
+ async function selectMany(rl, label, options, preselectedIds) {
263
+ if (!process.stdin.isTTY) {
264
+ return preselectedIds
265
+ }
266
+
267
+ const stdin = process.stdin
268
+ const stdout = process.stdout
269
+ let selectedIndex = 0
270
+ let selected = new Set(preselectedIds)
271
+ const totalLines = options.length + 3
272
+ let renderedOnce = false
273
+
274
+ const render = () => {
275
+ if (renderedOnce) {
276
+ stdout.write(`\x1b[${totalLines}F`)
277
+ } else {
278
+ stdout.write("\n")
279
+ }
280
+
281
+ stdout.write(`${ANSI_BOLD}${label}${ANSI_RESET}\n`)
282
+ for (let i = 0; i < options.length; i += 1) {
283
+ const option = options[i]
284
+ const isCursor = i === selectedIndex
285
+ const isSelected = selected.has(option.id)
286
+ const cursor = isCursor ? `${ANSI_BOLD}${ANSI_CYAN}>${ANSI_RESET}` : " "
287
+ const mark = isSelected ? `${ANSI_GREEN}[x]${ANSI_RESET}` : "[ ]"
288
+ const styleStart = isCursor ? `${ANSI_BOLD}${ANSI_CYAN}` : ""
289
+ const styleEnd = isCursor ? ANSI_RESET : ""
290
+ stdout.write(`${cursor} ${mark} ${styleStart}${option.label}${styleEnd} ${ANSI_DIM}(${option.id}) - ${option.description}${ANSI_RESET}\n`)
291
+ }
292
+ stdout.write(`${ANSI_DIM}Use Up/Down, Space to toggle, A to toggle all, Enter to confirm${ANSI_RESET}\n`)
293
+ renderedOnce = true
294
+ }
295
+
296
+ return new Promise((resolvePromise, rejectPromise) => {
297
+ const cleanup = () => {
298
+ stdin.off("keypress", onKeyPress)
299
+ if (stdin.isTTY) {
300
+ stdin.setRawMode(false)
301
+ }
302
+ stdout.write("\n")
303
+ }
304
+
305
+ const onKeyPress = (_, key) => {
306
+ if (!key) {
307
+ return
308
+ }
309
+
310
+ if (key.ctrl && key.name === "c") {
311
+ cleanup()
312
+ rejectPromise(new Error("Operation cancelled"))
313
+ return
314
+ }
315
+
316
+ if (key.name === "up") {
317
+ selectedIndex = selectedIndex === 0 ? options.length - 1 : selectedIndex - 1
318
+ render()
319
+ return
320
+ }
321
+
322
+ if (key.name === "down") {
323
+ selectedIndex = selectedIndex === options.length - 1 ? 0 : selectedIndex + 1
324
+ render()
325
+ return
326
+ }
327
+
328
+ if (key.name === "space") {
329
+ const id = options[selectedIndex].id
330
+ if (selected.has(id)) {
331
+ selected.delete(id)
332
+ } else {
333
+ selected.add(id)
334
+ }
335
+ render()
336
+ return
337
+ }
338
+
339
+ if (key.name === "a") {
340
+ if (selected.size === options.length) {
341
+ selected = new Set()
342
+ } else {
343
+ selected = new Set(options.map((option) => option.id))
344
+ }
345
+ render()
346
+ return
347
+ }
348
+
349
+ if (key.name === "return") {
350
+ cleanup()
351
+ resolvePromise([...selected])
352
+ }
353
+ }
354
+
355
+ emitKeypressEvents(stdin)
356
+ stdin.setRawMode(true)
357
+ stdin.resume()
358
+ stdin.on("keypress", onKeyPress)
359
+ render()
360
+ })
361
+ }
362
+
363
+ function getBaseSkillFrontmatter() {
364
+ return `---
365
+ name: cascadetui
366
+ description: Build and maintain terminal UIs with CascadeTUI. Use when creating components, layouts, renderables, keyboard interactions, and debugging terminal UI behavior in Cascade-based projects.
367
+ compatibility: Requires Bun and TypeScript. Designed for agents that support Agent Skills and SKILL.md frontmatter.
368
+ metadata:
369
+ author: cascadetui
370
+ version: "1.0"
371
+ ---`
372
+ }
373
+
374
+ function getBaseSkillBody() {
375
+ return `# CascadeTUI Skill
376
+
377
+ ## When To Use
378
+ - Use this skill when the user is building or debugging a terminal UI with CascadeTUI.
379
+ - Use this skill for component layout, rendering behavior, keyboard handling, and interaction logic.
380
+ - Use this skill for scaffold and setup guidance in Cascade projects.
381
+
382
+ ## Instructions
383
+ 1. Prefer Bun commands and APIs.
384
+ 2. Prefer \`@cascadetui/core\` for low-level renderables unless the user explicitly asks for React or Solid.
385
+ 3. Use \`@cascadetui/react\` or \`@cascadetui/solid\` only when requested.
386
+ 4. Keep code minimal, typed, deterministic, and production-ready.
387
+ 5. Reproduce rendering bugs with a minimal reproducible test case before applying fixes.
388
+ 6. Validate keyboard, focus, and resize behavior for interactive components.
389
+
390
+ ## Scaffolding
391
+ - \`bun create cascade my-app\`
392
+ - \`cd my-app\`
393
+ - \`bun install\`
394
+ - \`bun run dev\`
395
+
396
+ ## Notes
397
+ - Favor clear state transitions and predictable rendering.
398
+ - Avoid adding dependencies that duplicate Bun runtime capabilities.
399
+ `
400
+ }
401
+
402
+ function getClaudeAppendix() {
403
+ return `## Documentation Index
404
+ Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt
405
+ Use this file to discover all available pages before exploring further.
406
+
407
+ ## Claude Code Skill Notes
408
+ - Keep this skill in a global location: \`~/.claude/skills/cascadetui/SKILL.md\`.
409
+ - Skills can be auto-invoked by Claude when relevant, or manually via \`/cascadetui\`.
410
+ - Keep the skill content concise and place optional supporting files beside \`SKILL.md\` when needed.
411
+ `
412
+ }
413
+
414
+ function getCursorFrontmatter() {
415
+ return `---
416
+ name: cascadetui
417
+ description: Build and maintain terminal UIs with CascadeTUI. Use when creating components, layouts, renderables, keyboard interactions, and debugging terminal UI behavior in Cascade-based projects.
418
+ compatibility: Cursor Agent Skills format with global installation under ~/.cursor/skills.
419
+ metadata:
420
+ author: cascadetui
421
+ version: "1.0"
422
+ ---`
423
+ }
424
+
425
+ function getCursorAppendix() {
426
+ return `## Cursor Skill Notes
427
+ - Use global installation first: \`~/.cursor/skills/cascadetui/SKILL.md\`.
428
+ - This skill follows the Agent Skills standard with YAML frontmatter.
429
+ - Ask clarification questions when requirements are ambiguous.
430
+ - Keep supporting material in \`references/\`, \`scripts/\`, and \`assets/\` only if needed.
431
+ `
432
+ }
433
+
434
+ function getSkillContent(agent) {
435
+ if (agent.flavor === "claude") {
436
+ return `${getBaseSkillFrontmatter()}
437
+
438
+ ${getBaseSkillBody()}
439
+
440
+ ${getClaudeAppendix()}
441
+ `
442
+ }
443
+
444
+ if (agent.flavor === "cursor") {
445
+ return `${getCursorFrontmatter()}
446
+
447
+ ${getBaseSkillBody()}
448
+
449
+ ${getCursorAppendix()}
450
+ `
451
+ }
452
+
453
+ return `${getBaseSkillFrontmatter()}
454
+
455
+ ${getBaseSkillBody()}
456
+ `
457
+ }
458
+
459
+ function installSkill(agent, dryRun) {
460
+ const content = getSkillContent(agent)
461
+ const targetFile = agent.installPath
462
+ const targetDir = resolve(targetFile, "..")
463
+
464
+ if (dryRun) {
465
+ return { agent: agent.id, path: targetFile, written: false }
466
+ }
467
+
468
+ mkdirSync(targetDir, { recursive: true })
469
+ writeFileSync(targetFile, content)
470
+ return { agent: agent.id, path: targetFile, written: true }
471
+ }
472
+
473
+ async function resolveAgentsToInstall(rl, detectedAgents, options) {
474
+ if (options.agents.length > 0) {
475
+ validateAgentIds(options.agents, detectedAgents)
476
+ return options.agents
477
+ }
478
+
479
+ const detectedIds = detectedAgents.filter((agent) => agent.detected).map((agent) => agent.id)
480
+ if (options.allDetected) {
481
+ return detectedIds
482
+ }
483
+
484
+ if (!process.stdin.isTTY) {
485
+ return detectedIds
486
+ }
487
+
488
+ const selectable = toSelectableOptions(detectedAgents)
489
+ const preselected = detectedIds.length > 0 ? detectedIds : detectedAgents.slice(0, 1).map((agent) => agent.id)
490
+
491
+ console.log("")
492
+ if (detectedIds.length > 0) {
493
+ console.log(`Detected agents: ${detectedIds.join(", ")}`)
494
+ console.log(`Quick install command: npx create-cascade-skill --agents ${detectedIds.join(",")}`)
495
+ } else {
496
+ console.log("No agent was auto-detected. Select manually.")
497
+ }
498
+
499
+ const selected = await selectMany(rl, "Choose agent targets for CascadeTUI skill:", selectable, preselected)
500
+ if (selected.length > 0) {
501
+ return selected
502
+ }
503
+
504
+ const fallback = (await promptLine(rl, "No agent selected. Enter comma-separated IDs (or leave empty to cancel): ")).trim()
505
+ if (!fallback) {
506
+ return []
507
+ }
508
+
509
+ const parsed = unique(parseAgentsArg(fallback))
510
+ validateAgentIds(parsed, detectedAgents)
511
+ return parsed
512
+ }
513
+
514
+ async function main() {
515
+ const options = parseArgs(process.argv)
516
+ if (options.help) {
517
+ printHelp()
518
+ return
519
+ }
520
+
521
+ const agents = detectAgents(getAgents())
522
+ if (options.list) {
523
+ printList(agents)
524
+ return
525
+ }
526
+
527
+ const rl = createInterface({
528
+ input: process.stdin,
529
+ output: process.stdout,
530
+ })
531
+
532
+ try {
533
+ const selectedIds = await resolveAgentsToInstall(rl, agents, options)
534
+ if (selectedIds.length === 0) {
535
+ console.log("Nothing to install.")
536
+ return
537
+ }
538
+
539
+ const selectedAgents = agents.filter((agent) => selectedIds.includes(agent.id))
540
+ const results = selectedAgents.map((agent) => installSkill(agent, options.dryRun))
541
+
542
+ console.log("")
543
+ console.log(`${ANSI_BOLD}CascadeTUI skill installer${ANSI_RESET}`)
544
+ for (const result of results) {
545
+ const prefix = result.written ? `${ANSI_GREEN}installed${ANSI_RESET}` : `${ANSI_YELLOW}planned${ANSI_RESET}`
546
+ console.log(`- ${result.agent}: ${prefix} -> ${result.path}`)
547
+ }
548
+
549
+ if (options.dryRun) {
550
+ console.log("")
551
+ console.log("Dry run complete. Re-run without --dry-run to write files.")
552
+ }
553
+ } finally {
554
+ rl.close()
555
+ }
556
+ }
557
+
558
+ main().catch((error) => {
559
+ console.error(error instanceof Error ? error.message : String(error))
560
+ process.exit(1)
561
+ })
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "create-cascade-skill",
3
+ "version": "0.1.6",
4
+ "description": "Install CascadeTUI skills for external coding agents",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/kirosnn/cascade.git",
10
+ "directory": "packages/skill"
11
+ },
12
+ "bin": {
13
+ "create-cascade-skill": "index.js"
14
+ },
15
+ "files": [
16
+ "index.js",
17
+ "README.md"
18
+ ],
19
+ "scripts": {
20
+ "publish:pkg": "bun scripts/publish.ts"
21
+ }
22
+ }