create-cascade-skill 0.1.6 → 0.1.7
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/index.js +370 -283
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -1,44 +1,42 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const ANSI_YELLOW = "\x1b[33m"
|
|
2
|
+
import { existsSync, mkdirSync, 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";
|
|
16
15
|
|
|
17
16
|
function getHomeDirectory() {
|
|
18
|
-
return process.env.HOME || process.env.USERPROFILE || process.cwd()
|
|
17
|
+
return process.env.HOME || process.env.USERPROFILE || process.cwd();
|
|
19
18
|
}
|
|
20
19
|
|
|
21
20
|
function commandExists(command) {
|
|
22
|
-
const lookup = process.platform === "win32" ? "where" : "which"
|
|
23
|
-
const result = spawnSync(lookup, [command], { stdio: "ignore" })
|
|
24
|
-
return result.status === 0
|
|
21
|
+
const lookup = process.platform === "win32" ? "where" : "which";
|
|
22
|
+
const result = spawnSync(lookup, [command], { stdio: "ignore" });
|
|
23
|
+
return result.status === 0;
|
|
25
24
|
}
|
|
26
25
|
|
|
27
26
|
function parseAgentsArg(value) {
|
|
28
27
|
return value
|
|
29
28
|
.split(",")
|
|
30
29
|
.map((part) => part.trim().toLowerCase())
|
|
31
|
-
.filter((part) => part.length > 0)
|
|
30
|
+
.filter((part) => part.length > 0);
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
function unique(items) {
|
|
35
|
-
return [...new Set(items)]
|
|
34
|
+
return [...new Set(items)];
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
function getAgents() {
|
|
39
|
-
const home = getHomeDirectory()
|
|
40
|
-
const appData = process.env.APPDATA || join(home, "AppData", "Roaming")
|
|
41
|
-
|
|
38
|
+
const home = getHomeDirectory();
|
|
39
|
+
const appData = process.env.APPDATA || join(home, "AppData", "Roaming");
|
|
42
40
|
return [
|
|
43
41
|
{
|
|
44
42
|
id: "codex",
|
|
@@ -73,7 +71,14 @@ function getAgents() {
|
|
|
73
71
|
description: "Install in ~/.codeium/windsurf/skills/cascadetui/SKILL.md",
|
|
74
72
|
commands: ["windsurf"],
|
|
75
73
|
detectPaths: [join(home, ".codeium"), join(appData, "Codeium")],
|
|
76
|
-
installPath: join(
|
|
74
|
+
installPath: join(
|
|
75
|
+
home,
|
|
76
|
+
".codeium",
|
|
77
|
+
"windsurf",
|
|
78
|
+
"skills",
|
|
79
|
+
"cascadetui",
|
|
80
|
+
"SKILL.md"
|
|
81
|
+
),
|
|
77
82
|
flavor: "generic",
|
|
78
83
|
},
|
|
79
84
|
{
|
|
@@ -136,7 +141,14 @@ function getAgents() {
|
|
|
136
141
|
description: "Install in ~/.config/goose/skills/cascadetui/SKILL.md",
|
|
137
142
|
commands: ["goose"],
|
|
138
143
|
detectPaths: [join(home, ".config", "goose"), join(appData, "goose")],
|
|
139
|
-
installPath: join(
|
|
144
|
+
installPath: join(
|
|
145
|
+
home,
|
|
146
|
+
".config",
|
|
147
|
+
"goose",
|
|
148
|
+
"skills",
|
|
149
|
+
"cascadetui",
|
|
150
|
+
"SKILL.md"
|
|
151
|
+
),
|
|
140
152
|
flavor: "generic",
|
|
141
153
|
},
|
|
142
154
|
{
|
|
@@ -148,414 +160,489 @@ function getAgents() {
|
|
|
148
160
|
installPath: join(home, ".ami", "skills", "cascadetui", "SKILL.md"),
|
|
149
161
|
flavor: "generic",
|
|
150
162
|
},
|
|
151
|
-
]
|
|
163
|
+
];
|
|
152
164
|
}
|
|
153
165
|
|
|
154
166
|
function parseArgs(argv) {
|
|
155
|
-
const args = argv.slice(2)
|
|
167
|
+
const args = argv.slice(2);
|
|
156
168
|
const options = {
|
|
157
169
|
agents: [],
|
|
158
170
|
allDetected: false,
|
|
159
171
|
list: false,
|
|
160
172
|
dryRun: false,
|
|
161
173
|
help: false,
|
|
162
|
-
}
|
|
163
|
-
|
|
174
|
+
};
|
|
164
175
|
for (let i = 0; i < args.length; i += 1) {
|
|
165
|
-
const arg = args[i]
|
|
166
|
-
|
|
176
|
+
const arg = args[i];
|
|
167
177
|
if (arg === "--agents" || arg === "-a") {
|
|
168
|
-
options.agents.push(...parseAgentsArg(args[i + 1] || ""))
|
|
169
|
-
i += 1
|
|
170
|
-
continue
|
|
178
|
+
options.agents.push(...parseAgentsArg(args[i + 1] || ""));
|
|
179
|
+
i += 1;
|
|
180
|
+
continue;
|
|
171
181
|
}
|
|
172
182
|
if (arg.startsWith("--agents=")) {
|
|
173
|
-
options.agents.push(...parseAgentsArg(arg.slice("--agents=".length)))
|
|
174
|
-
continue
|
|
183
|
+
options.agents.push(...parseAgentsArg(arg.slice("--agents=".length)));
|
|
184
|
+
continue;
|
|
175
185
|
}
|
|
176
186
|
if (arg === "--all-detected") {
|
|
177
|
-
options.allDetected = true
|
|
178
|
-
continue
|
|
187
|
+
options.allDetected = true;
|
|
188
|
+
continue;
|
|
179
189
|
}
|
|
180
190
|
if (arg === "--list") {
|
|
181
|
-
options.list = true
|
|
182
|
-
continue
|
|
191
|
+
options.list = true;
|
|
192
|
+
continue;
|
|
183
193
|
}
|
|
184
194
|
if (arg === "--dry-run") {
|
|
185
|
-
options.dryRun = true
|
|
186
|
-
continue
|
|
195
|
+
options.dryRun = true;
|
|
196
|
+
continue;
|
|
187
197
|
}
|
|
188
198
|
if (arg === "--help" || arg === "-h") {
|
|
189
|
-
options.help = true
|
|
190
|
-
continue
|
|
199
|
+
options.help = true;
|
|
200
|
+
continue;
|
|
191
201
|
}
|
|
192
|
-
throw new Error(`Unknown argument: ${arg}`)
|
|
202
|
+
throw new Error(`Unknown argument: ${arg}`);
|
|
193
203
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
return options
|
|
204
|
+
options.agents = unique(options.agents);
|
|
205
|
+
return options;
|
|
197
206
|
}
|
|
198
207
|
|
|
199
208
|
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")
|
|
209
|
+
console.log("Usage: npx create-cascade-skill [options]");
|
|
210
|
+
console.log("");
|
|
211
|
+
console.log("Options:");
|
|
212
|
+
console.log(" -a, --agents <ids> Comma-separated agent IDs to install");
|
|
213
|
+
console.log(" --all-detected Install for all detected agents");
|
|
214
|
+
console.log(" --list Print supported and detected agents");
|
|
215
|
+
console.log(" --dry-run Preview files without writing");
|
|
216
|
+
console.log(" -h, --help Show help");
|
|
217
|
+
console.log("");
|
|
218
|
+
console.log("Examples:");
|
|
219
|
+
console.log(" npx create-cascade-skill");
|
|
220
|
+
console.log(" npx create-cascade-skill --all-detected");
|
|
221
|
+
console.log(" npx create-cascade-skill --agents codex,cursor,cline");
|
|
222
|
+
console.log(" npx create-cascade-skill --agents codex --dry-run");
|
|
214
223
|
}
|
|
215
224
|
|
|
216
225
|
function detectAgents(agents) {
|
|
217
226
|
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
|
-
})
|
|
227
|
+
const commandHit = agent.commands.some((command) => commandExists(command));
|
|
228
|
+
const pathHit = agent.detectPaths.some((path) => existsSync(path));
|
|
229
|
+
return { ...agent, detected: commandHit || pathHit };
|
|
230
|
+
});
|
|
222
231
|
}
|
|
223
232
|
|
|
224
233
|
function printList(agents) {
|
|
225
|
-
console.log(`${ANSI_BOLD}Supported agents${ANSI_RESET}`)
|
|
234
|
+
console.log(`${ANSI_BOLD}Supported agents${ANSI_RESET}`);
|
|
226
235
|
for (const agent of agents) {
|
|
227
|
-
const marker = agent.detected
|
|
228
|
-
|
|
236
|
+
const marker = agent.detected
|
|
237
|
+
? `${ANSI_GREEN}detected${ANSI_RESET}`
|
|
238
|
+
: `${ANSI_YELLOW}not detected${ANSI_RESET}`;
|
|
239
|
+
console.log(`- ${agent.id.padEnd(12)} ${marker} ${agent.label}`);
|
|
229
240
|
}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
241
|
+
const detected = agents
|
|
242
|
+
.filter((agent) => agent.detected)
|
|
243
|
+
.map((agent) => agent.id);
|
|
244
|
+
console.log("");
|
|
233
245
|
if (detected.length > 0) {
|
|
234
|
-
console.log(`Detected IDs: ${detected.join(", ")}`)
|
|
235
|
-
console.log(
|
|
246
|
+
console.log(`Detected IDs: ${detected.join(", ")}`);
|
|
247
|
+
console.log(
|
|
248
|
+
`Install all detected: npx create-cascade-skill --agents ${detected.join(
|
|
249
|
+
","
|
|
250
|
+
)}`
|
|
251
|
+
);
|
|
236
252
|
} else {
|
|
237
|
-
console.log("Detected IDs: none")
|
|
253
|
+
console.log("Detected IDs: none");
|
|
238
254
|
}
|
|
239
255
|
}
|
|
240
256
|
|
|
241
257
|
function validateAgentIds(selected, agents) {
|
|
242
|
-
const allowed = new Set(agents.map((agent) => agent.id))
|
|
258
|
+
const allowed = new Set(agents.map((agent) => agent.id));
|
|
243
259
|
for (const id of selected) {
|
|
244
260
|
if (!allowed.has(id)) {
|
|
245
|
-
throw new Error(`Unknown agent id: ${id}`)
|
|
261
|
+
throw new Error(`Unknown agent id: ${id}`);
|
|
246
262
|
}
|
|
247
263
|
}
|
|
248
264
|
}
|
|
249
265
|
|
|
250
266
|
function promptLine(rl, label) {
|
|
251
|
-
return rl.question(label)
|
|
267
|
+
return rl.question(label);
|
|
252
268
|
}
|
|
253
269
|
|
|
254
270
|
function toSelectableOptions(agents) {
|
|
255
271
|
return agents.map((agent) => ({
|
|
256
272
|
id: agent.id,
|
|
257
273
|
label: agent.label,
|
|
258
|
-
description: `${agent.description}${agent.detected ? " [detected]" : ""
|
|
259
|
-
|
|
274
|
+
description: `${agent.description}${agent.detected ? " [detected]" : ""
|
|
275
|
+
}`,
|
|
276
|
+
}));
|
|
260
277
|
}
|
|
261
278
|
|
|
262
279
|
async function selectMany(rl, label, options, preselectedIds) {
|
|
263
280
|
if (!process.stdin.isTTY) {
|
|
264
|
-
return preselectedIds
|
|
281
|
+
return preselectedIds;
|
|
265
282
|
}
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
let
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
let renderedOnce = false
|
|
273
|
-
|
|
283
|
+
const stdin = process.stdin;
|
|
284
|
+
const stdout = process.stdout;
|
|
285
|
+
let selectedIndex = 0;
|
|
286
|
+
let selected = new Set(preselectedIds);
|
|
287
|
+
const totalLines = options.length + 3;
|
|
288
|
+
let renderedOnce = false;
|
|
274
289
|
const render = () => {
|
|
275
290
|
if (renderedOnce) {
|
|
276
|
-
stdout.write(`\x1b[${totalLines}F`)
|
|
291
|
+
stdout.write(`\x1b[${totalLines}F`);
|
|
277
292
|
} else {
|
|
278
|
-
stdout.write("\n")
|
|
293
|
+
stdout.write("\n");
|
|
279
294
|
}
|
|
280
|
-
|
|
281
|
-
stdout.write(`${ANSI_BOLD}${label}${ANSI_RESET}\n`)
|
|
295
|
+
stdout.write(`${ANSI_BOLD}${label}${ANSI_RESET}\n`);
|
|
282
296
|
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
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
const
|
|
290
|
-
|
|
297
|
+
const option = options[i];
|
|
298
|
+
const isCursor = i === selectedIndex;
|
|
299
|
+
const isSelected = selected.has(option.id);
|
|
300
|
+
const cursor = isCursor
|
|
301
|
+
? `${ANSI_BOLD}${ANSI_CYAN}>${ANSI_RESET}`
|
|
302
|
+
: " ";
|
|
303
|
+
const mark = isSelected
|
|
304
|
+
? `${ANSI_GREEN}[x]${ANSI_RESET}`
|
|
305
|
+
: "[ ]";
|
|
306
|
+
const styleStart = isCursor ? `${ANSI_BOLD}${ANSI_CYAN}` : "";
|
|
307
|
+
const styleEnd = isCursor ? ANSI_RESET : "";
|
|
308
|
+
stdout.write(
|
|
309
|
+
`${cursor} ${mark} ${styleStart}${option.label}${styleEnd} ${ANSI_DIM}(${option.id}) - ${option.description}${ANSI_RESET}\n`
|
|
310
|
+
);
|
|
291
311
|
}
|
|
292
|
-
stdout.write(
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
312
|
+
stdout.write(
|
|
313
|
+
`${ANSI_DIM}Use Up/Down, Space to toggle, A to toggle all, Enter to confirm${ANSI_RESET}\n`
|
|
314
|
+
);
|
|
315
|
+
renderedOnce = true;
|
|
316
|
+
};
|
|
296
317
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
297
318
|
const cleanup = () => {
|
|
298
|
-
stdin.off("keypress", onKeyPress)
|
|
319
|
+
stdin.off("keypress", onKeyPress);
|
|
299
320
|
if (stdin.isTTY) {
|
|
300
|
-
stdin.setRawMode(false)
|
|
321
|
+
stdin.setRawMode(false);
|
|
301
322
|
}
|
|
302
|
-
stdout.write("\n")
|
|
303
|
-
}
|
|
304
|
-
|
|
323
|
+
stdout.write("\n");
|
|
324
|
+
};
|
|
305
325
|
const onKeyPress = (_, key) => {
|
|
306
326
|
if (!key) {
|
|
307
|
-
return
|
|
327
|
+
return;
|
|
308
328
|
}
|
|
309
|
-
|
|
310
329
|
if (key.ctrl && key.name === "c") {
|
|
311
|
-
cleanup()
|
|
312
|
-
rejectPromise(new Error("Operation cancelled"))
|
|
313
|
-
return
|
|
330
|
+
cleanup();
|
|
331
|
+
rejectPromise(new Error("Operation cancelled"));
|
|
332
|
+
return;
|
|
314
333
|
}
|
|
315
|
-
|
|
316
334
|
if (key.name === "up") {
|
|
317
|
-
selectedIndex =
|
|
318
|
-
|
|
319
|
-
|
|
335
|
+
selectedIndex =
|
|
336
|
+
selectedIndex === 0 ? options.length - 1 : selectedIndex - 1;
|
|
337
|
+
render();
|
|
338
|
+
return;
|
|
320
339
|
}
|
|
321
|
-
|
|
322
340
|
if (key.name === "down") {
|
|
323
|
-
selectedIndex =
|
|
324
|
-
|
|
325
|
-
|
|
341
|
+
selectedIndex =
|
|
342
|
+
selectedIndex === options.length - 1 ? 0 : selectedIndex + 1;
|
|
343
|
+
render();
|
|
344
|
+
return;
|
|
326
345
|
}
|
|
327
|
-
|
|
328
346
|
if (key.name === "space") {
|
|
329
|
-
const id = options[selectedIndex].id
|
|
347
|
+
const id = options[selectedIndex].id;
|
|
330
348
|
if (selected.has(id)) {
|
|
331
|
-
selected.delete(id)
|
|
349
|
+
selected.delete(id);
|
|
332
350
|
} else {
|
|
333
|
-
selected.add(id)
|
|
351
|
+
selected.add(id);
|
|
334
352
|
}
|
|
335
|
-
render()
|
|
336
|
-
return
|
|
353
|
+
render();
|
|
354
|
+
return;
|
|
337
355
|
}
|
|
338
|
-
|
|
339
356
|
if (key.name === "a") {
|
|
340
357
|
if (selected.size === options.length) {
|
|
341
|
-
selected = new Set()
|
|
358
|
+
selected = new Set();
|
|
342
359
|
} else {
|
|
343
|
-
selected = new Set(options.map((option) => option.id))
|
|
360
|
+
selected = new Set(options.map((option) => option.id));
|
|
344
361
|
}
|
|
345
|
-
render()
|
|
346
|
-
return
|
|
362
|
+
render();
|
|
363
|
+
return;
|
|
347
364
|
}
|
|
348
|
-
|
|
349
365
|
if (key.name === "return") {
|
|
350
|
-
cleanup()
|
|
351
|
-
resolvePromise([...selected])
|
|
366
|
+
cleanup();
|
|
367
|
+
resolvePromise([...selected]);
|
|
352
368
|
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
stdin.
|
|
357
|
-
stdin.
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
})
|
|
369
|
+
};
|
|
370
|
+
emitKeypressEvents(stdin);
|
|
371
|
+
stdin.setRawMode(true);
|
|
372
|
+
stdin.resume();
|
|
373
|
+
stdin.on("keypress", onKeyPress);
|
|
374
|
+
render();
|
|
375
|
+
});
|
|
361
376
|
}
|
|
362
377
|
|
|
363
|
-
function
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
compatibility
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
378
|
+
function getSkillFrontmatter(agent) {
|
|
379
|
+
const baseName = "cascadetui";
|
|
380
|
+
const description =
|
|
381
|
+
"Build terminal user interfaces with CascadeTUI. Use this skill when building, debugging, or refactoring Cascade-based TUIs. Triggers: Cascade, TUI, terminal UI, keyboard navigation, component layout.";
|
|
382
|
+
let compatibility = "Requires Bun and TypeScript.";
|
|
383
|
+
if (agent.flavor === "claude") {
|
|
384
|
+
compatibility += " Designed for Claude Code.";
|
|
385
|
+
} else if (agent.flavor === "cursor") {
|
|
386
|
+
compatibility += " Designed for Cursor.";
|
|
387
|
+
} else if (agent.flavor === "codex") {
|
|
388
|
+
compatibility += " Designed for OpenAI Codex.";
|
|
389
|
+
} else {
|
|
390
|
+
compatibility += ` Designed for ${agent.label}.`;
|
|
391
|
+
}
|
|
392
|
+
const allowedTools = "Bash(bun:*) Bash(npm:*) Bash(node:*)";
|
|
393
|
+
return (
|
|
394
|
+
`---\n` +
|
|
395
|
+
`name: ${baseName}\n` +
|
|
396
|
+
`description: ${description}\n` +
|
|
397
|
+
`compatibility: ${compatibility}\n` +
|
|
398
|
+
`allowed-tools: ${allowedTools}\n` +
|
|
399
|
+
`metadata:\n` +
|
|
400
|
+
` author: cascadetui\n` +
|
|
401
|
+
` version: "1.1"\n` +
|
|
402
|
+
`---`
|
|
403
|
+
);
|
|
372
404
|
}
|
|
373
405
|
|
|
374
|
-
function
|
|
375
|
-
return `# CascadeTUI
|
|
376
|
-
|
|
377
|
-
## When
|
|
378
|
-
|
|
379
|
-
- Use this skill
|
|
380
|
-
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
-
|
|
398
|
-
|
|
399
|
-
|
|
406
|
+
function getSkillBody() {
|
|
407
|
+
return `# Building Terminal UIs with CascadeTUI
|
|
408
|
+
|
|
409
|
+
## When to Activate
|
|
410
|
+
|
|
411
|
+
- Use this skill when asked to build, debug, or refactor a text-based user interface using the CascadeTUI library.
|
|
412
|
+
- Trigger this skill if the user mentions "Cascade", "TUI", "terminal UI", "keyboard navigation", or "component layout".
|
|
413
|
+
- Use when scaffolding new projects or customizing existing ones.
|
|
414
|
+
|
|
415
|
+
## Key Principles
|
|
416
|
+
|
|
417
|
+
- **Bun first**: Use the Bun runtime and its native package manager for running scripts and managing dependencies. Avoid using Node-specific commands or features unless absolutely required.
|
|
418
|
+
- **Core Library**: The \`@cascadetui/core\` package provides the primitives for rendering, layout, and input handling. Use it for basic applications. Only reach for the React (\`@cascadetui/react\`) or Solid (\`@cascadetui/solid\`) wrappers when explicitly requested.
|
|
419
|
+
- **Typed and deterministic**: Write TypeScript code with explicit types. Avoid dynamic evaluation and unpredictable side effects.
|
|
420
|
+
- **Minimal reproducible examples**: When diagnosing issues, start by isolating the problem in a tiny script or component that reproduces the bug. Only after reproducing should you propose fixes.
|
|
421
|
+
- **Interactive validation**: Test keyboard navigation, focus management, and resize events across multiple terminal sizes. Ensure accessible shortcuts and fallback navigation.
|
|
422
|
+
|
|
423
|
+
## Typical Workflow
|
|
424
|
+
|
|
425
|
+
1. **Scaffold a project**
|
|
426
|
+
Use Bun's project generator to create a new CascadeTUI app:
|
|
427
|
+
\`\`\`bash
|
|
428
|
+
bun create cascade my-app
|
|
429
|
+
cd my-app
|
|
430
|
+
bun install
|
|
431
|
+
bun run dev
|
|
432
|
+
\`\`\`
|
|
433
|
+
|
|
434
|
+
2. **Develop components**
|
|
435
|
+
- Keep components pure and functions side-effect free where possible.
|
|
436
|
+
- Compose layouts using containers (horizontal, vertical, grid) and ensure consistent spacing.
|
|
437
|
+
- Use dedicated input handlers rather than global listeners.
|
|
438
|
+
|
|
439
|
+
3. **Debug rendering**
|
|
440
|
+
- If a component fails to render or update, add logging and ensure the state drives the UI deterministically.
|
|
441
|
+
- Recreate the failing state in isolation.
|
|
442
|
+
- Check for improper asynchronous updates.
|
|
443
|
+
|
|
444
|
+
4. **Keyboard & interaction**
|
|
445
|
+
- Provide intuitive key bindings with documentation.
|
|
446
|
+
- Implement focus rings or highlight states.
|
|
447
|
+
- Handle edge cases like simultaneous key presses, terminal resize, and loss of focus.
|
|
448
|
+
|
|
449
|
+
5. **Performance considerations**
|
|
450
|
+
- Avoid expensive re-renders inside tight loops.
|
|
451
|
+
- Batch state updates where possible.
|
|
452
|
+
- Profile for memory leaks or event listener accumulation.
|
|
453
|
+
|
|
454
|
+
## Best Practices
|
|
455
|
+
|
|
456
|
+
- **State management**: Use a predictable state container or simple \`useState\`-like patterns. Avoid hidden state.
|
|
457
|
+
- **Testing**: Write unit tests for components and integration tests for complex flows. Use snapshot tests carefully.
|
|
458
|
+
- **Documentation**: Document all public-facing components with usage examples and list supported props.
|
|
459
|
+
- **Clean-up**: Remove unused dependencies and scripts. Keep the codebase tidy for readability.
|
|
460
|
+
|
|
461
|
+
## Additional Resources
|
|
462
|
+
|
|
463
|
+
- Official CascadeTUI documentation (if available).
|
|
464
|
+
- Bun runtime documentation: https://bun.sh/docs for details on Bun-specific APIs.
|
|
465
|
+
- Example projects in the CascadeTUI GitHub repository (if available).
|
|
466
|
+
`;
|
|
400
467
|
}
|
|
401
468
|
|
|
402
|
-
function
|
|
469
|
+
function getImprovedClaudeAppendix() {
|
|
403
470
|
return `## Documentation Index
|
|
404
|
-
|
|
405
|
-
Use this file to discover all available pages before exploring further.
|
|
471
|
+
|
|
472
|
+
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
473
|
|
|
407
474
|
## 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
475
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
version: "1.0"
|
|
422
|
-
---`
|
|
476
|
+
- Place this skill in a global location: \`~/.claude/skills/cascadetui/SKILL.md\`. Do not nest additional directories deeper than one level.
|
|
477
|
+
- Claude automatically loads skills whose \`name\` and \`description\` fields match the user's request; keep them descriptive and concise.
|
|
478
|
+
- Follow the naming conventions from the Agent Skills specification: lowercase alphanumeric and hyphens only, no reserved words (anthropic, claude).
|
|
479
|
+
- Use progressive disclosure: keep \`SKILL.md\` under 500 lines and move lengthy instructions to \`references/\` files.
|
|
480
|
+
- Ask clarifying questions when requirements are ambiguous or when multiple interpretations exist.
|
|
481
|
+
- Keep scripts deterministic and self-contained; avoid side effects that require external network access unless absolutely necessary.
|
|
482
|
+
`;
|
|
423
483
|
}
|
|
424
484
|
|
|
425
|
-
function
|
|
485
|
+
function getImprovedCursorAppendix() {
|
|
426
486
|
return `## Cursor Skill Notes
|
|
427
|
-
|
|
428
|
-
-
|
|
429
|
-
-
|
|
430
|
-
-
|
|
431
|
-
|
|
487
|
+
|
|
488
|
+
- Install this skill globally under \`~/.cursor/skills/cascadetui/SKILL.md\`.
|
|
489
|
+
- Cursor uses the open Agent Skills format with YAML frontmatter; ensure the \`name\` and \`description\` fields align with your directory name and skill triggers.
|
|
490
|
+
- Ask clarifying questions when user requests lack details.
|
|
491
|
+
- Keep supporting materials in \`references/\`, \`scripts/\`, and \`assets/\` for progressive disclosure.
|
|
492
|
+
- Avoid referencing frameworks (React, Solid) unless specifically requested by the user.
|
|
493
|
+
- Use determinism and idempotent commands; Cursor may re-run instructions if the output is ambiguous.
|
|
494
|
+
`;
|
|
432
495
|
}
|
|
433
496
|
|
|
434
|
-
function
|
|
435
|
-
|
|
436
|
-
return `${getBaseSkillFrontmatter()}
|
|
497
|
+
function getGenericAppendix(agent) {
|
|
498
|
+
return `## ${agent.label} Skill Notes
|
|
437
499
|
|
|
438
|
-
|
|
500
|
+
- Install this skill globally under the agent's skills directory (for example, \`${agent.installPath}\`).
|
|
501
|
+
- 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.
|
|
502
|
+
- Use progressive disclosure: place detailed guides, examples, or scripts in \`references/\` and \`scripts/\` directories to minimise the size of \`SKILL.md\`.
|
|
503
|
+
- Ask for clarification when the user's request is ambiguous.
|
|
504
|
+
- Follow the principles outlined in this skill for minimal, typed, and deterministic code.`;
|
|
505
|
+
}
|
|
439
506
|
|
|
440
|
-
|
|
441
|
-
|
|
507
|
+
function getSkillContent(agent) {
|
|
508
|
+
const frontmatter = getSkillFrontmatter(agent);
|
|
509
|
+
const body = getSkillBody();
|
|
510
|
+
if (agent.flavor === "claude") {
|
|
511
|
+
return (
|
|
512
|
+
frontmatter +
|
|
513
|
+
"\n\n" +
|
|
514
|
+
body +
|
|
515
|
+
"\n\n" +
|
|
516
|
+
getImprovedClaudeAppendix()
|
|
517
|
+
);
|
|
442
518
|
}
|
|
443
|
-
|
|
444
519
|
if (agent.flavor === "cursor") {
|
|
445
|
-
return
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
${getCursorAppendix()}
|
|
450
|
-
`
|
|
520
|
+
return (
|
|
521
|
+
frontmatter + "\n\n" + body + "\n\n" + getImprovedCursorAppendix()
|
|
522
|
+
);
|
|
451
523
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
${getBaseSkillBody()}
|
|
456
|
-
`
|
|
524
|
+
return (
|
|
525
|
+
frontmatter + "\n\n" + body + "\n\n" + getGenericAppendix(agent)
|
|
526
|
+
);
|
|
457
527
|
}
|
|
458
528
|
|
|
459
529
|
function installSkill(agent, dryRun) {
|
|
460
|
-
const content = getSkillContent(agent)
|
|
461
|
-
const targetFile = agent.installPath
|
|
462
|
-
const targetDir = resolve(targetFile, "..")
|
|
463
|
-
|
|
530
|
+
const content = getSkillContent(agent);
|
|
531
|
+
const targetFile = agent.installPath;
|
|
532
|
+
const targetDir = resolve(targetFile, "..");
|
|
464
533
|
if (dryRun) {
|
|
465
|
-
return { agent: agent.id, path: targetFile, written: false }
|
|
534
|
+
return { agent: agent.id, path: targetFile, written: false };
|
|
466
535
|
}
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
return { agent: agent.id, path: targetFile, written: true }
|
|
536
|
+
mkdirSync(targetDir, { recursive: true });
|
|
537
|
+
writeFileSync(targetFile, content);
|
|
538
|
+
return { agent: agent.id, path: targetFile, written: true };
|
|
471
539
|
}
|
|
472
540
|
|
|
473
541
|
async function resolveAgentsToInstall(rl, detectedAgents, options) {
|
|
474
542
|
if (options.agents.length > 0) {
|
|
475
|
-
validateAgentIds(options.agents, detectedAgents)
|
|
476
|
-
return options.agents
|
|
543
|
+
validateAgentIds(options.agents, detectedAgents);
|
|
544
|
+
return options.agents;
|
|
477
545
|
}
|
|
478
|
-
|
|
479
|
-
|
|
546
|
+
const detectedIds = detectedAgents
|
|
547
|
+
.filter((agent) => agent.detected)
|
|
548
|
+
.map((agent) => agent.id);
|
|
480
549
|
if (options.allDetected) {
|
|
481
|
-
return detectedIds
|
|
550
|
+
return detectedIds;
|
|
482
551
|
}
|
|
483
|
-
|
|
484
552
|
if (!process.stdin.isTTY) {
|
|
485
|
-
return detectedIds
|
|
553
|
+
return detectedIds;
|
|
486
554
|
}
|
|
487
|
-
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
555
|
+
const selectable = toSelectableOptions(detectedAgents);
|
|
556
|
+
const preselected =
|
|
557
|
+
detectedIds.length > 0
|
|
558
|
+
? detectedIds
|
|
559
|
+
: detectedAgents.slice(0, 1).map((agent) => agent.id);
|
|
560
|
+
console.log("");
|
|
492
561
|
if (detectedIds.length > 0) {
|
|
493
|
-
console.log(`Detected agents: ${detectedIds.join(", ")}`)
|
|
494
|
-
console.log(
|
|
562
|
+
console.log(`Detected agents: ${detectedIds.join(", ")}`);
|
|
563
|
+
console.log(
|
|
564
|
+
`Quick install command: npx create-cascade-skill --agents ${detectedIds.join(
|
|
565
|
+
","
|
|
566
|
+
)}`
|
|
567
|
+
);
|
|
495
568
|
} else {
|
|
496
|
-
console.log("No agent was auto-detected. Select manually.")
|
|
569
|
+
console.log("No agent was auto-detected. Select manually.");
|
|
497
570
|
}
|
|
498
|
-
|
|
499
|
-
|
|
571
|
+
const selected = await selectMany(
|
|
572
|
+
rl,
|
|
573
|
+
"Choose agent targets for CascadeTUI skill:",
|
|
574
|
+
selectable,
|
|
575
|
+
preselected
|
|
576
|
+
);
|
|
500
577
|
if (selected.length > 0) {
|
|
501
|
-
return selected
|
|
578
|
+
return selected;
|
|
502
579
|
}
|
|
503
|
-
|
|
504
|
-
|
|
580
|
+
const fallback = (
|
|
581
|
+
await promptLine(
|
|
582
|
+
rl,
|
|
583
|
+
"No agent selected. Enter comma-separated IDs (or leave empty to cancel): "
|
|
584
|
+
)
|
|
585
|
+
)
|
|
586
|
+
.trim();
|
|
505
587
|
if (!fallback) {
|
|
506
|
-
return []
|
|
588
|
+
return [];
|
|
507
589
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
return parsed
|
|
590
|
+
const parsed = unique(parseAgentsArg(fallback));
|
|
591
|
+
validateAgentIds(parsed, detectedAgents);
|
|
592
|
+
return parsed;
|
|
512
593
|
}
|
|
513
594
|
|
|
514
595
|
async function main() {
|
|
515
|
-
const options = parseArgs(process.argv)
|
|
596
|
+
const options = parseArgs(process.argv);
|
|
516
597
|
if (options.help) {
|
|
517
|
-
printHelp()
|
|
518
|
-
return
|
|
598
|
+
printHelp();
|
|
599
|
+
return;
|
|
519
600
|
}
|
|
520
|
-
|
|
521
|
-
const agents = detectAgents(getAgents())
|
|
601
|
+
const agents = detectAgents(getAgents());
|
|
522
602
|
if (options.list) {
|
|
523
|
-
printList(agents)
|
|
524
|
-
return
|
|
603
|
+
printList(agents);
|
|
604
|
+
return;
|
|
525
605
|
}
|
|
526
|
-
|
|
527
606
|
const rl = createInterface({
|
|
528
607
|
input: process.stdin,
|
|
529
608
|
output: process.stdout,
|
|
530
|
-
})
|
|
531
|
-
|
|
609
|
+
});
|
|
532
610
|
try {
|
|
533
|
-
const selectedIds = await resolveAgentsToInstall(rl, agents, options)
|
|
611
|
+
const selectedIds = await resolveAgentsToInstall(rl, agents, options);
|
|
534
612
|
if (selectedIds.length === 0) {
|
|
535
|
-
console.log("Nothing to install.")
|
|
536
|
-
return
|
|
613
|
+
console.log("Nothing to install.");
|
|
614
|
+
return;
|
|
537
615
|
}
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
616
|
+
const selectedAgents = agents.filter((agent) =>
|
|
617
|
+
selectedIds.includes(agent.id)
|
|
618
|
+
);
|
|
619
|
+
const results = selectedAgents.map((agent) =>
|
|
620
|
+
installSkill(agent, options.dryRun)
|
|
621
|
+
);
|
|
622
|
+
console.log("");
|
|
623
|
+
console.log(`${ANSI_BOLD}CascadeTUI skill installer${ANSI_RESET}`);
|
|
544
624
|
for (const result of results) {
|
|
545
|
-
const prefix = result.written
|
|
546
|
-
|
|
625
|
+
const prefix = result.written
|
|
626
|
+
? `${ANSI_GREEN}installed${ANSI_RESET}`
|
|
627
|
+
: `${ANSI_YELLOW}planned${ANSI_RESET}`;
|
|
628
|
+
console.log(
|
|
629
|
+
`- ${result.agent}: ${prefix} -> ${result.path}`
|
|
630
|
+
);
|
|
547
631
|
}
|
|
548
|
-
|
|
549
632
|
if (options.dryRun) {
|
|
550
|
-
console.log("")
|
|
551
|
-
console.log(
|
|
633
|
+
console.log("");
|
|
634
|
+
console.log(
|
|
635
|
+
"Dry run complete. Re-run without --dry-run to write files."
|
|
636
|
+
);
|
|
552
637
|
}
|
|
553
638
|
} finally {
|
|
554
|
-
rl.close()
|
|
639
|
+
rl.close();
|
|
555
640
|
}
|
|
556
641
|
}
|
|
557
642
|
|
|
558
643
|
main().catch((error) => {
|
|
559
|
-
console.error(
|
|
560
|
-
|
|
561
|
-
|
|
644
|
+
console.error(
|
|
645
|
+
error instanceof Error ? error.message : String(error)
|
|
646
|
+
);
|
|
647
|
+
process.exit(1);
|
|
648
|
+
});
|