mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-02 19:32:43 +00:00
- 16 cross-shell utilities converted to sh/bash chezmoi executables: ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj (ghg-clone not named ghg: unrelated cargo binary owns that name) - cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files - fish keeps only shell-state functions; converted ones now hit PATH - cp-appid ignore gating moved to .local/bin path - dot_agents/skills: 82 skills materialized from dotstate storage so dot_claude skill symlinks resolve on fresh bootstrap; dotstate profile/symlinks.json released .agents/skills .bashrc .zshrc
332 lines
9.2 KiB
TypeScript
332 lines
9.2 KiB
TypeScript
#!/usr/bin/env -S deno run --allow-run --allow-read --allow-write
|
|
|
|
/**
|
|
* Generates markdown documentation from `linear --help` output.
|
|
* Run periodically as the CLI evolves to keep skill references up to date.
|
|
*/
|
|
|
|
import { dirname, join } from "@std/path"
|
|
|
|
const SCRIPT_DIR = dirname(new URL(import.meta.url).pathname)
|
|
const SKILL_DIR = join(SCRIPT_DIR, "..")
|
|
const REFERENCES_DIR = join(SKILL_DIR, "references")
|
|
const SKILL_MD = join(SKILL_DIR, "SKILL.md")
|
|
const SKILL_TEMPLATE = join(SKILL_DIR, "SKILL.template.md")
|
|
|
|
// Files to preserve (not generated from help)
|
|
const PRESERVED_FILES = ["organization-features.md"]
|
|
|
|
// Commands to skip (shell completions, not useful for docs)
|
|
const SKIP_COMMANDS = ["completions"]
|
|
|
|
interface CommandInfo {
|
|
name: string
|
|
description: string
|
|
help: string
|
|
subcommands: CommandInfo[]
|
|
}
|
|
|
|
async function run(
|
|
cmd: string[],
|
|
): Promise<{ success: boolean; stdout: string; stderr: string }> {
|
|
const command = new Deno.Command(cmd[0], {
|
|
args: cmd.slice(1),
|
|
stdout: "piped",
|
|
stderr: "piped",
|
|
env: { NO_COLOR: "1" }, // Disable ANSI colors
|
|
})
|
|
const result = await command.output()
|
|
return {
|
|
success: result.success,
|
|
stdout: new TextDecoder().decode(result.stdout).trim(),
|
|
stderr: new TextDecoder().decode(result.stderr).trim(),
|
|
}
|
|
}
|
|
|
|
function stripAnsi(str: string): string {
|
|
// Remove ANSI escape codes (in case NO_COLOR doesn't work)
|
|
// deno-lint-ignore no-control-regex
|
|
return str.replace(/\x1b\[[0-9;]*m/g, "")
|
|
}
|
|
|
|
function stripVersion(str: string): string {
|
|
// Remove "Version: X.Y.Z" lines from help output to avoid churn on version bumps
|
|
return str.replace(/^Version:.*\n?/gm, "").replace(/\n+$/, "")
|
|
}
|
|
|
|
function parseCommands(helpText: string): string[] {
|
|
const commands: string[] = []
|
|
const lines = helpText.split("\n")
|
|
let inCommands = false
|
|
let foundAnyCommand = false
|
|
|
|
for (const line of lines) {
|
|
if (line.startsWith("Commands:")) {
|
|
inCommands = true
|
|
continue
|
|
}
|
|
if (inCommands) {
|
|
// Command lines look like: " command, alias - Description"
|
|
// or: " command <arg> - Description"
|
|
// Capture command name (stopping at comma or whitespace)
|
|
const match = line.match(/^\s{2}([a-z][-a-z]*)(?:,|\s)/)
|
|
if (match) {
|
|
commands.push(match[1])
|
|
foundAnyCommand = true
|
|
} else if (foundAnyCommand && line.trim() === "") {
|
|
// Empty line after we've found commands means end of section
|
|
break
|
|
}
|
|
// Skip empty lines before first command (common in help output)
|
|
}
|
|
}
|
|
|
|
return commands
|
|
}
|
|
|
|
async function getCommandHelp(cmdPath: string[]): Promise<string> {
|
|
const result = await run(["linear", ...cmdPath, "--help"])
|
|
if (!result.success) {
|
|
return result.stderr || "Command help not available"
|
|
}
|
|
return stripVersion(stripAnsi(result.stdout))
|
|
}
|
|
|
|
async function discoverCommand(cmdPath: string[]): Promise<CommandInfo> {
|
|
const help = await getCommandHelp(cmdPath)
|
|
const name = cmdPath.join(" ")
|
|
|
|
// Extract description from help text
|
|
const descMatch = help.match(/Description:\s*\n\s*(.+)/)
|
|
const description = descMatch ? descMatch[1].trim() : ""
|
|
|
|
// Find subcommands
|
|
const subcommandNames = parseCommands(help)
|
|
const subcommands: CommandInfo[] = []
|
|
|
|
for (const subcmd of subcommandNames) {
|
|
const subInfo = await discoverCommand([...cmdPath, subcmd])
|
|
subcommands.push(subInfo)
|
|
}
|
|
|
|
return { name, description, help, subcommands }
|
|
}
|
|
|
|
function formatCommandMarkdown(cmd: CommandInfo, depth = 0): string {
|
|
const lines: string[] = []
|
|
const cmdName = cmd.name.replace(/^linear /, "")
|
|
const heading = depth === 0 ? "#" : "##"
|
|
|
|
lines.push(`${heading} linear ${cmdName}`)
|
|
lines.push("")
|
|
lines.push("```")
|
|
lines.push(cmd.help)
|
|
lines.push("```")
|
|
lines.push("")
|
|
|
|
// Add subcommands as separate sections
|
|
for (const sub of cmd.subcommands) {
|
|
lines.push(formatCommandMarkdown(sub, depth + 1))
|
|
}
|
|
|
|
return lines.join("\n")
|
|
}
|
|
|
|
function generateCommandDoc(cmd: CommandInfo): string {
|
|
const lines: string[] = []
|
|
const cmdName = cmd.name.replace(/^linear /, "")
|
|
|
|
lines.push(`# ${cmdName}`)
|
|
lines.push("")
|
|
lines.push(`> ${cmd.description}`)
|
|
lines.push("")
|
|
lines.push("## Usage")
|
|
lines.push("")
|
|
lines.push("```")
|
|
lines.push(cmd.help)
|
|
lines.push("```")
|
|
|
|
// Add subcommand details
|
|
if (cmd.subcommands.length > 0) {
|
|
lines.push("")
|
|
lines.push("## Subcommands")
|
|
|
|
for (const sub of cmd.subcommands) {
|
|
const subName = sub.name.split(" ").pop()!
|
|
lines.push("")
|
|
lines.push(`### ${subName}`)
|
|
lines.push("")
|
|
if (sub.description) {
|
|
lines.push(`> ${sub.description}`)
|
|
lines.push("")
|
|
}
|
|
lines.push("```")
|
|
lines.push(sub.help)
|
|
lines.push("```")
|
|
|
|
// Handle 3-level deep commands (e.g., issue comment add)
|
|
if (sub.subcommands.length > 0) {
|
|
lines.push("")
|
|
lines.push(`#### ${subName} subcommands`)
|
|
|
|
for (const subsub of sub.subcommands) {
|
|
const subsubName = subsub.name.split(" ").pop()!
|
|
lines.push("")
|
|
lines.push(`##### ${subsubName}`)
|
|
lines.push("")
|
|
lines.push("```")
|
|
lines.push(subsub.help)
|
|
lines.push("```")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return lines.join("\n")
|
|
}
|
|
|
|
async function main() {
|
|
console.log("Generating Linear CLI documentation...")
|
|
|
|
// Check linear is available
|
|
const versionResult = await run(["linear", "--version"])
|
|
if (!versionResult.success) {
|
|
console.error("Error: linear CLI not found. Is it installed?")
|
|
Deno.exit(1)
|
|
}
|
|
console.log(`Linear CLI: ${stripAnsi(versionResult.stdout)}`)
|
|
|
|
// Auto-discover top-level commands from `linear --help`
|
|
console.log("Discovering commands...")
|
|
const topLevelHelp = await getCommandHelp([])
|
|
const topLevelCommands = parseCommands(topLevelHelp).filter(
|
|
(cmd) => !SKIP_COMMANDS.includes(cmd),
|
|
)
|
|
console.log(`Found ${topLevelCommands.length} top-level commands`)
|
|
|
|
const commands: CommandInfo[] = []
|
|
|
|
for (const cmd of topLevelCommands) {
|
|
console.log(` Discovering: ${cmd}`)
|
|
const info = await discoverCommand([cmd])
|
|
commands.push(info)
|
|
}
|
|
|
|
// Generate markdown files
|
|
console.log("Generating markdown files...")
|
|
|
|
// Ensure references directory exists
|
|
await Deno.mkdir(REFERENCES_DIR, { recursive: true })
|
|
|
|
// Get list of preserved files to keep
|
|
const preservedPaths = new Set(
|
|
PRESERVED_FILES.map((f) => join(REFERENCES_DIR, f)),
|
|
)
|
|
|
|
// Clean up old generated files (but preserve manual files)
|
|
for await (const entry of Deno.readDir(REFERENCES_DIR)) {
|
|
const filePath = join(REFERENCES_DIR, entry.name)
|
|
if (!preservedPaths.has(filePath) && entry.name.endsWith(".md")) {
|
|
await Deno.remove(filePath)
|
|
}
|
|
}
|
|
|
|
// Write command documentation
|
|
for (const cmd of commands) {
|
|
const filename = `${cmd.name.replace(/^linear /, "")}.md`
|
|
const filepath = join(REFERENCES_DIR, filename)
|
|
const content = generateCommandDoc(cmd)
|
|
await Deno.writeTextFile(filepath, content + "\n")
|
|
console.log(` Generated: ${filename}`)
|
|
}
|
|
|
|
// Generate index file
|
|
const indexContent = generateIndex(commands)
|
|
await Deno.writeTextFile(join(REFERENCES_DIR, "commands.md"), indexContent)
|
|
console.log(" Generated: commands.md")
|
|
|
|
// Generate SKILL.md from template
|
|
console.log("Generating SKILL.md from template...")
|
|
const skillContent = await generateSkillMd(commands)
|
|
await Deno.writeTextFile(SKILL_MD, skillContent)
|
|
console.log(" Generated: SKILL.md")
|
|
|
|
// Format all generated files
|
|
console.log("\nFormatting generated files...")
|
|
const fmtResult = await run([
|
|
"deno",
|
|
"fmt",
|
|
SKILL_DIR,
|
|
])
|
|
if (!fmtResult.success) {
|
|
console.error("Warning: Failed to format generated files")
|
|
console.error(fmtResult.stderr)
|
|
}
|
|
|
|
console.log(`\nDone! Generated ${commands.length + 2} files.`)
|
|
}
|
|
|
|
function generateIndex(commands: CommandInfo[]): string {
|
|
const lines: string[] = []
|
|
|
|
lines.push("# Linear CLI Command Reference")
|
|
lines.push("")
|
|
lines.push("## Commands")
|
|
lines.push("")
|
|
|
|
for (const cmd of commands) {
|
|
const cmdName = cmd.name.replace(/^linear /, "")
|
|
lines.push(`- [${cmdName}](./${cmdName}.md) - ${cmd.description}`)
|
|
}
|
|
|
|
lines.push("")
|
|
lines.push("## Quick Reference")
|
|
lines.push("")
|
|
lines.push("```bash")
|
|
lines.push("# Get help for any command")
|
|
lines.push("linear <command> --help")
|
|
lines.push("linear <command> <subcommand> --help")
|
|
lines.push("```")
|
|
|
|
return lines.join("\n") + "\n"
|
|
}
|
|
|
|
function flattenCommandPaths(cmd: CommandInfo): string[] {
|
|
const lines = [`linear ${cmd.name}`]
|
|
|
|
for (const sub of cmd.subcommands) {
|
|
lines.push(...flattenCommandPaths(sub))
|
|
}
|
|
|
|
return lines
|
|
}
|
|
|
|
function generateCommandsSection(commands: CommandInfo[]): string {
|
|
return commands
|
|
.map((cmd) => flattenCommandPaths(cmd).join("\n"))
|
|
.join("\n\n")
|
|
}
|
|
|
|
function generateReferenceToc(commands: CommandInfo[]): string {
|
|
const lines: string[] = []
|
|
|
|
for (const cmd of commands) {
|
|
lines.push(
|
|
`- [${cmd.name}](references/${cmd.name}.md) - ${cmd.description}`,
|
|
)
|
|
}
|
|
|
|
return lines.join("\n")
|
|
}
|
|
|
|
async function generateSkillMd(
|
|
commands: CommandInfo[],
|
|
): Promise<string> {
|
|
const template = await Deno.readTextFile(SKILL_TEMPLATE)
|
|
return template
|
|
.replace("{{COMMANDS}}", generateCommandsSection(commands))
|
|
.replace("{{REFERENCE_TOC}}", generateReferenceToc(commands))
|
|
}
|
|
|
|
main()
|