create-cascade-skill 0.1.6 → 0.1.8

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 (2) hide show
  1. package/index.js +502 -287
  2. package/package.json +3 -2
package/index.js CHANGED
@@ -1,44 +1,54 @@
1
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()
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
+ import { resolve, join } from "node:path";
4
+ import process from "node:process";
5
+ import { spawnSync } from "node:child_process";
6
+ import { createInterface } from "node:readline/promises";
7
+ import { emitKeypressEvents } from "node:readline";
8
+
9
+ const ANSI_RESET = "\x1b[0m";
10
+ const ANSI_BOLD = "\x1b[1m";
11
+ const ANSI_DIM = "\x1b[2m";
12
+ const ANSI_CYAN = "\x1b[36m";
13
+ const ANSI_GREEN = "\x1b[32m";
14
+ const ANSI_YELLOW = "\x1b[33m";
15
+
16
+ function getHomeDirectory(options) {
17
+ if (options?.home) {
18
+ return options.home;
19
+ }
20
+ return (
21
+ process.env.CASCADE_SKILL_HOME ||
22
+ process.env.HOME ||
23
+ process.env.USERPROFILE ||
24
+ process.cwd()
25
+ );
19
26
  }
20
27
 
21
28
  function commandExists(command) {
22
- const lookup = process.platform === "win32" ? "where" : "which"
23
- const result = spawnSync(lookup, [command], { stdio: "ignore" })
24
- return result.status === 0
29
+ const lookup = process.platform === "win32" ? "where" : "which";
30
+ const result = spawnSync(lookup, [command], { stdio: "ignore" });
31
+ return result.status === 0;
25
32
  }
26
33
 
27
34
  function parseAgentsArg(value) {
28
35
  return value
29
36
  .split(",")
30
37
  .map((part) => part.trim().toLowerCase())
31
- .filter((part) => part.length > 0)
38
+ .filter((part) => part.length > 0);
32
39
  }
33
40
 
34
41
  function unique(items) {
35
- return [...new Set(items)]
42
+ return [...new Set(items)];
36
43
  }
37
44
 
38
- function getAgents() {
39
- const home = getHomeDirectory()
40
- const appData = process.env.APPDATA || join(home, "AppData", "Roaming")
41
-
45
+ function getAgents(options) {
46
+ const home = getHomeDirectory(options);
47
+ const appData =
48
+ options?.appData ||
49
+ process.env.CASCADE_SKILL_APPDATA ||
50
+ process.env.APPDATA ||
51
+ join(home, "AppData", "Roaming");
42
52
  return [
43
53
  {
44
54
  id: "codex",
@@ -67,13 +77,29 @@ function getAgents() {
67
77
  installPath: join(home, ".cursor", "skills", "cascadetui", "SKILL.md"),
68
78
  flavor: "cursor",
69
79
  },
80
+ {
81
+ id: "factory",
82
+ label: "Factory (Droid CLI)",
83
+ description: "Install in ~/.factory/skills/cascadetui/SKILL.md",
84
+ commands: ["droid"],
85
+ detectPaths: [join(home, ".factory")],
86
+ installPath: join(home, ".factory", "skills", "cascadetui", "SKILL.md"),
87
+ flavor: "factory",
88
+ },
70
89
  {
71
90
  id: "windsurf",
72
91
  label: "Windsurf",
73
92
  description: "Install in ~/.codeium/windsurf/skills/cascadetui/SKILL.md",
74
93
  commands: ["windsurf"],
75
94
  detectPaths: [join(home, ".codeium"), join(appData, "Codeium")],
76
- installPath: join(home, ".codeium", "windsurf", "skills", "cascadetui", "SKILL.md"),
95
+ installPath: join(
96
+ home,
97
+ ".codeium",
98
+ "windsurf",
99
+ "skills",
100
+ "cascadetui",
101
+ "SKILL.md"
102
+ ),
77
103
  flavor: "generic",
78
104
  },
79
105
  {
@@ -136,7 +162,14 @@ function getAgents() {
136
162
  description: "Install in ~/.config/goose/skills/cascadetui/SKILL.md",
137
163
  commands: ["goose"],
138
164
  detectPaths: [join(home, ".config", "goose"), join(appData, "goose")],
139
- installPath: join(home, ".config", "goose", "skills", "cascadetui", "SKILL.md"),
165
+ installPath: join(
166
+ home,
167
+ ".config",
168
+ "goose",
169
+ "skills",
170
+ "cascadetui",
171
+ "SKILL.md"
172
+ ),
140
173
  flavor: "generic",
141
174
  },
142
175
  {
@@ -148,107 +181,129 @@ function getAgents() {
148
181
  installPath: join(home, ".ami", "skills", "cascadetui", "SKILL.md"),
149
182
  flavor: "generic",
150
183
  },
151
- ]
184
+ ];
152
185
  }
153
186
 
154
187
  function parseArgs(argv) {
155
- const args = argv.slice(2)
188
+ const args = argv.slice(2);
156
189
  const options = {
157
190
  agents: [],
158
191
  allDetected: false,
159
192
  list: false,
160
193
  dryRun: false,
194
+ force: false,
161
195
  help: false,
162
- }
163
-
196
+ home: undefined,
197
+ };
164
198
  for (let i = 0; i < args.length; i += 1) {
165
- const arg = args[i]
166
-
199
+ const arg = args[i];
167
200
  if (arg === "--agents" || arg === "-a") {
168
- options.agents.push(...parseAgentsArg(args[i + 1] || ""))
169
- i += 1
170
- continue
201
+ options.agents.push(...parseAgentsArg(args[i + 1] || ""));
202
+ i += 1;
203
+ continue;
171
204
  }
172
205
  if (arg.startsWith("--agents=")) {
173
- options.agents.push(...parseAgentsArg(arg.slice("--agents=".length)))
174
- continue
206
+ options.agents.push(...parseAgentsArg(arg.slice("--agents=".length)));
207
+ continue;
175
208
  }
176
209
  if (arg === "--all-detected") {
177
- options.allDetected = true
178
- continue
210
+ options.allDetected = true;
211
+ continue;
179
212
  }
180
213
  if (arg === "--list") {
181
- options.list = true
182
- continue
214
+ options.list = true;
215
+ continue;
183
216
  }
184
217
  if (arg === "--dry-run") {
185
- options.dryRun = true
186
- continue
218
+ options.dryRun = true;
219
+ continue;
220
+ }
221
+ if (arg === "--force") {
222
+ options.force = true;
223
+ continue;
224
+ }
225
+ if (arg === "--home") {
226
+ options.home = args[i + 1];
227
+ i += 1;
228
+ continue;
229
+ }
230
+ if (arg.startsWith("--home=")) {
231
+ options.home = arg.slice("--home=".length);
232
+ continue;
187
233
  }
188
234
  if (arg === "--help" || arg === "-h") {
189
- options.help = true
190
- continue
235
+ options.help = true;
236
+ continue;
191
237
  }
192
- throw new Error(`Unknown argument: ${arg}`)
238
+ throw new Error(`Unknown argument: ${arg}`);
193
239
  }
194
-
195
- options.agents = unique(options.agents)
196
- return options
240
+ options.agents = unique(options.agents);
241
+ return options;
197
242
  }
198
243
 
199
244
  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")
245
+ console.log("Usage: npx create-cascade-skill [options]");
246
+ console.log("");
247
+ console.log("Options:");
248
+ console.log(" -a, --agents <ids> Comma-separated agent IDs to install");
249
+ console.log(" --all-detected Install for all detected agents");
250
+ console.log(" --list Print supported and detected agents");
251
+ console.log(" --dry-run Preview files without writing");
252
+ console.log(" --force Overwrite SKILL.md when it differs");
253
+ console.log(" --home <path> Override home directory used for detection/install");
254
+ console.log(" -h, --help Show help");
255
+ console.log("");
256
+ console.log("Examples:");
257
+ console.log(" npx create-cascade-skill");
258
+ console.log(" npx create-cascade-skill --all-detected");
259
+ console.log(" npx create-cascade-skill --agents codex,cursor,cline");
260
+ console.log(" npx create-cascade-skill --agents codex --dry-run");
261
+ console.log(" npx create-cascade-skill --agents windsurf --home ./sandbox --dry-run");
214
262
  }
215
263
 
216
264
  function detectAgents(agents) {
217
265
  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
- })
266
+ const commandHit = agent.commands.some((command) => commandExists(command));
267
+ const pathHit = agent.detectPaths.some((path) => existsSync(path));
268
+ return { ...agent, detected: commandHit || pathHit };
269
+ });
222
270
  }
223
271
 
224
272
  function printList(agents) {
225
- console.log(`${ANSI_BOLD}Supported agents${ANSI_RESET}`)
273
+ console.log(`${ANSI_BOLD}Supported agents${ANSI_RESET}`);
226
274
  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}`)
275
+ const marker = agent.detected
276
+ ? `${ANSI_GREEN}detected${ANSI_RESET}`
277
+ : `${ANSI_YELLOW}not detected${ANSI_RESET}`;
278
+ console.log(`- ${agent.id.padEnd(12)} ${marker} ${agent.label}`);
229
279
  }
230
-
231
- const detected = agents.filter((agent) => agent.detected).map((agent) => agent.id)
232
- console.log("")
280
+ const detected = agents
281
+ .filter((agent) => agent.detected)
282
+ .map((agent) => agent.id);
283
+ console.log("");
233
284
  if (detected.length > 0) {
234
- console.log(`Detected IDs: ${detected.join(", ")}`)
235
- console.log(`Install all detected: npx create-cascade-skill --agents ${detected.join(",")}`)
285
+ console.log(`Detected IDs: ${detected.join(", ")}`);
286
+ console.log(
287
+ `Install all detected: npx create-cascade-skill --agents ${detected.join(
288
+ ","
289
+ )}`
290
+ );
236
291
  } else {
237
- console.log("Detected IDs: none")
292
+ console.log("Detected IDs: none");
238
293
  }
239
294
  }
240
295
 
241
296
  function validateAgentIds(selected, agents) {
242
- const allowed = new Set(agents.map((agent) => agent.id))
297
+ const allowed = new Set(agents.map((agent) => agent.id));
243
298
  for (const id of selected) {
244
299
  if (!allowed.has(id)) {
245
- throw new Error(`Unknown agent id: ${id}`)
300
+ throw new Error(`Unknown agent id: ${id}`);
246
301
  }
247
302
  }
248
303
  }
249
304
 
250
305
  function promptLine(rl, label) {
251
- return rl.question(label)
306
+ return rl.question(label);
252
307
  }
253
308
 
254
309
  function toSelectableOptions(agents) {
@@ -256,306 +311,466 @@ function toSelectableOptions(agents) {
256
311
  id: agent.id,
257
312
  label: agent.label,
258
313
  description: `${agent.description}${agent.detected ? " [detected]" : ""}`,
259
- }))
314
+ }));
260
315
  }
261
316
 
262
317
  async function selectMany(rl, label, options, preselectedIds) {
263
318
  if (!process.stdin.isTTY) {
264
- return preselectedIds
319
+ return preselectedIds;
265
320
  }
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
-
321
+ const stdin = process.stdin;
322
+ const stdout = process.stdout;
323
+ let selectedIndex = 0;
324
+ let selected = new Set(preselectedIds);
325
+ const totalLines = options.length + 3;
326
+ let renderedOnce = false;
274
327
  const render = () => {
275
328
  if (renderedOnce) {
276
- stdout.write(`\x1b[${totalLines}F`)
329
+ stdout.write(`\x1b[${totalLines}F`);
277
330
  } else {
278
- stdout.write("\n")
331
+ stdout.write("\n");
279
332
  }
280
-
281
- stdout.write(`${ANSI_BOLD}${label}${ANSI_RESET}\n`)
333
+ stdout.write(`${ANSI_BOLD}${label}${ANSI_RESET}\n`);
282
334
  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`)
335
+ const option = options[i];
336
+ const isCursor = i === selectedIndex;
337
+ const isSelected = selected.has(option.id);
338
+ const cursor = isCursor
339
+ ? `${ANSI_BOLD}${ANSI_CYAN}>${ANSI_RESET}`
340
+ : " ";
341
+ const mark = isSelected ? `${ANSI_GREEN}[x]${ANSI_RESET}` : "[ ]";
342
+ const styleStart = isCursor ? `${ANSI_BOLD}${ANSI_CYAN}` : "";
343
+ const styleEnd = isCursor ? ANSI_RESET : "";
344
+ stdout.write(
345
+ `${cursor} ${mark} ${styleStart}${option.label}${styleEnd} ${ANSI_DIM}(${option.id}) - ${option.description}${ANSI_RESET}\n`
346
+ );
291
347
  }
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
-
348
+ stdout.write(
349
+ `${ANSI_DIM}Use Up/Down, Space to toggle, A to toggle all, Enter to confirm${ANSI_RESET}\n`
350
+ );
351
+ renderedOnce = true;
352
+ };
296
353
  return new Promise((resolvePromise, rejectPromise) => {
297
354
  const cleanup = () => {
298
- stdin.off("keypress", onKeyPress)
355
+ stdin.off("keypress", onKeyPress);
299
356
  if (stdin.isTTY) {
300
- stdin.setRawMode(false)
357
+ stdin.setRawMode(false);
301
358
  }
302
- stdout.write("\n")
303
- }
304
-
359
+ stdout.write("\n");
360
+ };
305
361
  const onKeyPress = (_, key) => {
306
362
  if (!key) {
307
- return
363
+ return;
308
364
  }
309
-
310
365
  if (key.ctrl && key.name === "c") {
311
- cleanup()
312
- rejectPromise(new Error("Operation cancelled"))
313
- return
366
+ cleanup();
367
+ rejectPromise(new Error("Operation cancelled"));
368
+ return;
314
369
  }
315
-
316
370
  if (key.name === "up") {
317
- selectedIndex = selectedIndex === 0 ? options.length - 1 : selectedIndex - 1
318
- render()
319
- return
371
+ selectedIndex =
372
+ selectedIndex === 0 ? options.length - 1 : selectedIndex - 1;
373
+ render();
374
+ return;
320
375
  }
321
-
322
376
  if (key.name === "down") {
323
- selectedIndex = selectedIndex === options.length - 1 ? 0 : selectedIndex + 1
324
- render()
325
- return
377
+ selectedIndex =
378
+ selectedIndex === options.length - 1 ? 0 : selectedIndex + 1;
379
+ render();
380
+ return;
326
381
  }
327
-
328
382
  if (key.name === "space") {
329
- const id = options[selectedIndex].id
383
+ const id = options[selectedIndex].id;
330
384
  if (selected.has(id)) {
331
- selected.delete(id)
385
+ selected.delete(id);
332
386
  } else {
333
- selected.add(id)
387
+ selected.add(id);
334
388
  }
335
- render()
336
- return
389
+ render();
390
+ return;
337
391
  }
338
-
339
392
  if (key.name === "a") {
340
393
  if (selected.size === options.length) {
341
- selected = new Set()
394
+ selected = new Set();
342
395
  } else {
343
- selected = new Set(options.map((option) => option.id))
396
+ selected = new Set(options.map((option) => option.id));
344
397
  }
345
- render()
346
- return
398
+ render();
399
+ return;
347
400
  }
348
-
349
401
  if (key.name === "return") {
350
- cleanup()
351
- resolvePromise([...selected])
402
+ cleanup();
403
+ resolvePromise([...selected]);
352
404
  }
353
- }
354
-
355
- emitKeypressEvents(stdin)
356
- stdin.setRawMode(true)
357
- stdin.resume()
358
- stdin.on("keypress", onKeyPress)
359
- render()
360
- })
405
+ };
406
+ emitKeypressEvents(stdin);
407
+ stdin.setRawMode(true);
408
+ stdin.resume();
409
+ stdin.on("keypress", onKeyPress);
410
+ render();
411
+ });
361
412
  }
362
413
 
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
- ---`
414
+ function getSkillFrontmatter(agent) {
415
+ const baseName = "cascadetui";
416
+ const description =
417
+ "Build terminal user interfaces with CascadeTUI. Use this skill to scaffold, debug, and refactor Cascade-based TUIs (layout, input, rendering, keyboard navigation, React/Solid bindings). Triggers: Cascade, CascadeTUI, TUI, terminal UI, keybindings, focus, renderer.";
418
+ let compatibility = "Requires Bun and TypeScript.";
419
+ if (agent.flavor === "claude") {
420
+ compatibility += " Designed for Claude Code.";
421
+ } else if (agent.flavor === "cursor") {
422
+ compatibility += " Designed for Cursor.";
423
+ } else if (agent.flavor === "codex") {
424
+ compatibility += " Designed for OpenAI Codex.";
425
+ } else if (agent.flavor === "factory") {
426
+ compatibility += " Designed for Factory (Droid CLI).";
427
+ } else {
428
+ compatibility += ` Designed for ${agent.label}.`;
429
+ }
430
+
431
+ const allowedTools =
432
+ agent.flavor === "factory"
433
+ ? "Read, Bash, Write"
434
+ : "Bash(bun:*) Bash(npm:*) Bash(node:*)";
435
+
436
+ const extraFactoryFrontmatter =
437
+ agent.flavor === "factory"
438
+ ? `\nuser-invocable: true\ndisable-model-invocation: false`
439
+ : "";
440
+
441
+ return (
442
+ `---\n` +
443
+ `name: ${baseName}\n` +
444
+ `description: ${description}\n` +
445
+ `compatibility: ${compatibility}\n` +
446
+ `allowed-tools: ${allowedTools}` +
447
+ `${extraFactoryFrontmatter}\n` +
448
+ `metadata:\n` +
449
+ ` author: cascadetui\n` +
450
+ ` version: "1.2"\n` +
451
+ `---`
452
+ );
372
453
  }
373
454
 
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
- `
455
+ function getSkillBody() {
456
+ return `# CascadeTUI Engineering Skill
457
+
458
+ ## Use This When
459
+
460
+ Activate for tasks involving:
461
+ - Building a new terminal UI (TUI) with CascadeTUI
462
+ - Fixing layout, rendering glitches, or resize bugs
463
+ - Keyboard navigation, focus, selection, shortcuts, input handling
464
+ - React/Solid bindings on top of CascadeTUI core
465
+ - Performance issues (re-render storms, slow lists) or state determinism
466
+
467
+ ## Output Expectations
468
+
469
+ When implementing or refactoring, produce:
470
+ - A minimal, runnable entrypoint that demonstrates the behavior
471
+ - Deterministic state updates and predictable render cycles
472
+ - Clear keybindings and focus behavior
473
+ - A short verification checklist (commands + manual steps)
474
+
475
+ ## Project Workflow (Bun-first)
476
+
477
+ 1) Ensure dependencies
478
+ \`\`\`bash
479
+ bun install
480
+ \`\`\`
481
+
482
+ 2) Run the app (or a repro script)
483
+ \`\`\`bash
484
+ bun run dev
485
+ \`\`\`
486
+
487
+ 3) Add a tiny repro when debugging
488
+ - Create \`scripts/repro.ts\` or a minimal app entrypoint
489
+ - Keep it self-contained: one screen, one interaction, one bug
490
+
491
+ ## Design Rules (CascadeTUI-specific)
492
+
493
+ ### Deterministic UI
494
+ - Treat rendering as a pure function of state
495
+ - Avoid hidden mutable globals for UI state
496
+ - Prefer single source of truth (one store or a small set of state atoms)
497
+
498
+ ### Layout & Composition
499
+ - Compose screens with containers and consistent spacing
500
+ - Keep one responsibility per component: layout vs input vs domain logic
501
+ - Use stable keys for lists; avoid index keys if items can move
502
+
503
+ ### Input, Focus, and Navigation
504
+ - Define a keymap per screen (Up/Down, Enter, Esc, Tab, Ctrl shortcuts)
505
+ - Always document primary actions and an escape/back path
506
+ - Ensure focus is explicit: which element receives keys right now
507
+ - Handle terminal resize: reflow layout and keep selection stable
508
+
509
+ ### Rendering & Performance
510
+ - Avoid rebuilding large trees on every keypress
511
+ - For large lists: paginate, virtualize, or reduce per-row computation
512
+ - Batch state updates; avoid cascading updates during render
513
+
514
+ ## Debugging Playbook
515
+
516
+ When something is wrong:
517
+ 1) Confirm the bug in a tiny repro
518
+ 2) Log state transitions around the interaction
519
+ 3) Verify input events fire once (no duplicated handlers)
520
+ 4) Verify keys/ids are stable (especially lists)
521
+ 5) Verify resize behavior by changing terminal size rapidly
522
+
523
+ Common failure modes:
524
+ - Duplicate listeners attached on re-render
525
+ - Non-stable list keys causing selection jumps
526
+ - Async state updates racing; UI shows stale selection
527
+ - Layout constraints (width/height) not propagated as expected
528
+
529
+ ## Quick Recipes
530
+
531
+ ### Add a consistent keymap footer
532
+ - Show the active shortcuts at the bottom (e.g. \`q\` quit, \`/\` search, arrows navigate)
533
+ - Keep it updated per screen
534
+
535
+ ### Search + List pattern
536
+ - Input line at top
537
+ - Filtered list in the middle
538
+ - Details/preview panel (optional)
539
+ - Enter selects, Esc clears/back
540
+
541
+ ### React binding guidance
542
+ - Keep bridge components thin
543
+ - Avoid passing unstable props that trigger full-tree rerenders
544
+ - Prefer memoization at boundaries (list row, heavy panels)
545
+
546
+ ## Verification Checklist
547
+
548
+ Run:
549
+ \`\`\`bash
550
+ bun run typecheck
551
+ bun run lint
552
+ bun test
553
+ \`\`\`
554
+
555
+ Manual:
556
+ - Start app in small and large terminals
557
+ - Resize while a list item is selected
558
+ - Navigate with keyboard only
559
+ - Confirm exit behavior (Ctrl+C and explicit quit key)
560
+ `;
400
561
  }
401
562
 
402
- function getClaudeAppendix() {
563
+ function getImprovedClaudeAppendix() {
403
564
  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.
565
+
566
+ Fetch the complete documentation index at: https://code.claude.com/docs/llms.txt. Use this file to discover all available pages before exploring further.
406
567
 
407
568
  ## 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
569
 
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
- ---`
570
+ - Place this skill in a global location: \`~/.claude/skills/cascadetui/SKILL.md\`. Do not nest additional directories deeper than one level.
571
+ - Claude automatically loads skills whose \`name\` and \`description\` fields match the user's request; keep them descriptive and concise.
572
+ - Follow the naming conventions from the Agent Skills specification: lowercase alphanumeric and hyphens only, no reserved words (anthropic, claude).
573
+ - Use progressive disclosure: keep \`SKILL.md\` under 500 lines and move lengthy instructions to \`references/\` files.
574
+ - Ask clarifying questions when requirements are ambiguous or when multiple interpretations exist.
575
+ - Keep scripts deterministic and self-contained; avoid side effects that require external network access unless absolutely necessary.
576
+ `;
423
577
  }
424
578
 
425
- function getCursorAppendix() {
579
+ function getImprovedCursorAppendix() {
426
580
  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
- `
581
+
582
+ - Install this skill globally under \`~/.cursor/skills/cascadetui/SKILL.md\`.
583
+ - Cursor uses the open Agent Skills format with YAML frontmatter; ensure the \`name\` and \`description\` fields align with your directory name and skill triggers.
584
+ - Ask clarifying questions when user requests lack details.
585
+ - Keep supporting materials in \`references/\`, \`scripts/\`, and \`assets/\` for progressive disclosure.
586
+ - Avoid referencing frameworks (React, Solid) unless specifically requested by the user.
587
+ - Use determinism and idempotent commands; Cursor may re-run instructions if the output is ambiguous.
588
+ `;
432
589
  }
433
590
 
434
- function getSkillContent(agent) {
435
- if (agent.flavor === "claude") {
436
- return `${getBaseSkillFrontmatter()}
591
+ function getImprovedFactoryAppendix() {
592
+ return `## Factory (Droid CLI) Skill Notes
593
+
594
+ - Skills are discovered from:
595
+ - Workspace: \`<repo>/.factory/skills/<skill-name>/SKILL.md\`
596
+ - Personal: \`~/.factory/skills/<skill-name>/SKILL.md\`
597
+ - Compatibility: \`<repo>/.agent/skills/\` :contentReference[oaicite:1]{index=1}
598
+ - This installer writes to the personal location by default: \`~/.factory/skills/cascadetui/SKILL.md\`.
599
+ - If you want to share the skill with teammates, copy it into your repo under \`.factory/skills/cascadetui/SKILL.md\` and commit it. :contentReference[oaicite:2]{index=2}
600
+ - Invocation control:
601
+ - \`disable-model-invocation: true\` to require manual \`/cascadetui\` invocation
602
+ - \`user-invocable: false\` to hide it from slash commands and keep it model-only :contentReference[oaicite:3]{index=3}
603
+ - Restart \`droid\` after adding/updating skills so it rescans them. :contentReference[oaicite:4]{index=4}
604
+ `;
605
+ }
437
606
 
438
- ${getBaseSkillBody()}
607
+ function getGenericAppendix(agent) {
608
+ return `## ${agent.label} Skill Notes
439
609
 
440
- ${getClaudeAppendix()}
441
- `
442
- }
610
+ - Install this skill globally under the agent's skills directory (for example, \`${agent.installPath}\`).
611
+ - This agent supports the open Agent Skills format; ensure the \`name\` and \`description\` fields match the directory name and capture when to trigger this skill.
612
+ - Use progressive disclosure: place detailed guides, examples, or scripts in \`references/\` and \`scripts/\` directories to minimise the size of \`SKILL.md\`.
613
+ - Ask for clarification when the user's request is ambiguous.
614
+ - Follow the principles outlined in this skill for minimal, typed, and deterministic code.`;
615
+ }
443
616
 
617
+ function getSkillContent(agent) {
618
+ const frontmatter = getSkillFrontmatter(agent);
619
+ const body = getSkillBody();
620
+ if (agent.flavor === "claude") {
621
+ return frontmatter + "\n\n" + body + "\n\n" + getImprovedClaudeAppendix();
622
+ }
444
623
  if (agent.flavor === "cursor") {
445
- return `${getCursorFrontmatter()}
446
-
447
- ${getBaseSkillBody()}
448
-
449
- ${getCursorAppendix()}
450
- `
624
+ return frontmatter + "\n\n" + body + "\n\n" + getImprovedCursorAppendix();
451
625
  }
452
-
453
- return `${getBaseSkillFrontmatter()}
454
-
455
- ${getBaseSkillBody()}
456
- `
626
+ if (agent.flavor === "factory") {
627
+ return frontmatter + "\n\n" + body + "\n\n" + getImprovedFactoryAppendix();
628
+ }
629
+ return frontmatter + "\n\n" + body + "\n\n" + getGenericAppendix(agent);
457
630
  }
458
631
 
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 }
632
+ function installSkill(agent, options) {
633
+ const content = getSkillContent(agent);
634
+ const targetFile = agent.installPath;
635
+ const targetDir = resolve(targetFile, "..");
636
+ if (options.dryRun) {
637
+ return { agent: agent.id, path: targetFile, written: false, skipped: false, reason: "dry-run" };
466
638
  }
467
-
468
- mkdirSync(targetDir, { recursive: true })
469
- writeFileSync(targetFile, content)
470
- return { agent: agent.id, path: targetFile, written: true }
639
+ if (existsSync(targetFile)) {
640
+ try {
641
+ const existing = readFileSync(targetFile, "utf8");
642
+ if (existing === content) {
643
+ return {
644
+ agent: agent.id,
645
+ path: targetFile,
646
+ written: false,
647
+ skipped: true,
648
+ reason: "already up-to-date",
649
+ };
650
+ }
651
+ if (!options.force) {
652
+ return {
653
+ agent: agent.id,
654
+ path: targetFile,
655
+ written: false,
656
+ skipped: true,
657
+ reason: "exists (use --force to overwrite)",
658
+ };
659
+ }
660
+ } catch {
661
+ if (!options.force) {
662
+ return {
663
+ agent: agent.id,
664
+ path: targetFile,
665
+ written: false,
666
+ skipped: true,
667
+ reason: "exists (unreadable; use --force to overwrite)",
668
+ };
669
+ }
670
+ }
671
+ }
672
+ mkdirSync(targetDir, { recursive: true });
673
+ writeFileSync(targetFile, content);
674
+ return { agent: agent.id, path: targetFile, written: true, skipped: false, reason: "written" };
471
675
  }
472
676
 
473
677
  async function resolveAgentsToInstall(rl, detectedAgents, options) {
474
678
  if (options.agents.length > 0) {
475
- validateAgentIds(options.agents, detectedAgents)
476
- return options.agents
679
+ validateAgentIds(options.agents, detectedAgents);
680
+ return options.agents;
477
681
  }
478
-
479
- const detectedIds = detectedAgents.filter((agent) => agent.detected).map((agent) => agent.id)
682
+ const detectedIds = detectedAgents
683
+ .filter((agent) => agent.detected)
684
+ .map((agent) => agent.id);
480
685
  if (options.allDetected) {
481
- return detectedIds
686
+ return detectedIds;
482
687
  }
483
-
484
688
  if (!process.stdin.isTTY) {
485
- return detectedIds
689
+ return detectedIds;
486
690
  }
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("")
691
+ const selectable = toSelectableOptions(detectedAgents);
692
+ const preselected =
693
+ detectedIds.length > 0
694
+ ? detectedIds
695
+ : detectedAgents.slice(0, 1).map((agent) => agent.id);
696
+ console.log("");
492
697
  if (detectedIds.length > 0) {
493
- console.log(`Detected agents: ${detectedIds.join(", ")}`)
494
- console.log(`Quick install command: npx create-cascade-skill --agents ${detectedIds.join(",")}`)
698
+ console.log(`Detected agents: ${detectedIds.join(", ")}`);
699
+ console.log(
700
+ `Quick install command: npx create-cascade-skill --agents ${detectedIds.join(
701
+ ","
702
+ )}`
703
+ );
495
704
  } else {
496
- console.log("No agent was auto-detected. Select manually.")
705
+ console.log("No agent was auto-detected. Select manually.");
497
706
  }
498
-
499
- const selected = await selectMany(rl, "Choose agent targets for CascadeTUI skill:", selectable, preselected)
707
+ const selected = await selectMany(
708
+ rl,
709
+ "Choose agent targets for CascadeTUI skill:",
710
+ selectable,
711
+ preselected
712
+ );
500
713
  if (selected.length > 0) {
501
- return selected
714
+ return selected;
502
715
  }
503
-
504
- const fallback = (await promptLine(rl, "No agent selected. Enter comma-separated IDs (or leave empty to cancel): ")).trim()
716
+ const fallback = (
717
+ await promptLine(
718
+ rl,
719
+ "No agent selected. Enter comma-separated IDs (or leave empty to cancel): "
720
+ )
721
+ ).trim();
505
722
  if (!fallback) {
506
- return []
723
+ return [];
507
724
  }
508
-
509
- const parsed = unique(parseAgentsArg(fallback))
510
- validateAgentIds(parsed, detectedAgents)
511
- return parsed
725
+ const parsed = unique(parseAgentsArg(fallback));
726
+ validateAgentIds(parsed, detectedAgents);
727
+ return parsed;
512
728
  }
513
729
 
514
730
  async function main() {
515
- const options = parseArgs(process.argv)
731
+ const options = parseArgs(process.argv);
516
732
  if (options.help) {
517
- printHelp()
518
- return
733
+ printHelp();
734
+ return;
519
735
  }
520
-
521
- const agents = detectAgents(getAgents())
736
+ const agents = detectAgents(getAgents({ home: options.home }));
522
737
  if (options.list) {
523
- printList(agents)
524
- return
738
+ printList(agents);
739
+ return;
525
740
  }
526
-
527
741
  const rl = createInterface({
528
742
  input: process.stdin,
529
743
  output: process.stdout,
530
- })
531
-
744
+ });
532
745
  try {
533
- const selectedIds = await resolveAgentsToInstall(rl, agents, options)
746
+ const selectedIds = await resolveAgentsToInstall(rl, agents, options);
534
747
  if (selectedIds.length === 0) {
535
- console.log("Nothing to install.")
536
- return
748
+ console.log("Nothing to install.");
749
+ return;
537
750
  }
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}`)
751
+ const selectedAgents = agents.filter((agent) => selectedIds.includes(agent.id));
752
+ const results = selectedAgents.map((agent) => installSkill(agent, options));
753
+ console.log("");
754
+ console.log(`${ANSI_BOLD}CascadeTUI skill installer${ANSI_RESET}`);
544
755
  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}`)
756
+ const prefix = result.written
757
+ ? `${ANSI_GREEN}installed${ANSI_RESET}`
758
+ : result.skipped
759
+ ? `${ANSI_YELLOW}skipped${ANSI_RESET}`
760
+ : `${ANSI_YELLOW}planned${ANSI_RESET}`;
761
+ const suffix = result.reason ? ` (${result.reason})` : "";
762
+ console.log(`- ${result.agent}: ${prefix} -> ${result.path}${suffix}`);
547
763
  }
548
-
549
764
  if (options.dryRun) {
550
- console.log("")
551
- console.log("Dry run complete. Re-run without --dry-run to write files.")
765
+ console.log("");
766
+ console.log("Dry run complete. Re-run without --dry-run to write files.");
552
767
  }
553
768
  } finally {
554
- rl.close()
769
+ rl.close();
555
770
  }
556
771
  }
557
772
 
558
773
  main().catch((error) => {
559
- console.error(error instanceof Error ? error.message : String(error))
560
- process.exit(1)
561
- })
774
+ console.error(error instanceof Error ? error.message : String(error));
775
+ process.exit(1);
776
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-cascade-skill",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Install CascadeTUI skills for external coding agents",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -17,6 +17,7 @@
17
17
  "README.md"
18
18
  ],
19
19
  "scripts": {
20
- "publish:pkg": "bun scripts/publish.ts"
20
+ "publish:pkg": "bun scripts/publish.ts",
21
+ "test:skill": "bun scripts/user-test.ts"
21
22
  }
22
23
  }