Skip to content

Commit b65a932

Browse files
feat(root): wire single-file linting to CLI
Integrate single-file mode into root command: Usage modes: - Full scan: cclint [component-type] - Single file: cclint ./path/to/file.md - Multiple files: cclint a.md b.md c.md - Explicit: cclint --file agents (for files named "agents") - Type override: cclint --type agent ./custom/file.md New flags: - --file: Explicit file paths (bypasses subcommand detection) - --type/-t: Force component type (agent|command|skill|settings|context|plugin) Flow: 1. collectFilesToLint() determines mode (single-file vs full scan) 2. runSingleFileLint() handles file mode with exit code 2 for invocation errors 3. runLint() handles full scan mode (exit code 1 for lint errors) Exit codes: - 0: Success (no errors) - 1: Lint errors found - 2: Invocation error (file not found, invalid type, etc.)
1 parent 23cb332 commit b65a932

1 file changed

Lines changed: 126 additions & 6 deletions

File tree

cmd/root.go

Lines changed: 126 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,25 +20,64 @@ var (
2020
outputFormat string
2121
outputFile string
2222
failOn string
23+
fileFlag []string // Explicit file paths (--file flag)
24+
typeFlag string // Force component type (--type flag)
2325
)
2426

2527
var rootCmd = &cobra.Command{
26-
Use: "cclint",
28+
Use: "cclint [files...]",
2729
Short: "Claude Code Lint - A comprehensive linting tool for Claude Code projects",
2830
Long: `CCLint is a linting tool for Claude Code projects that validates agent files,
2931
command files, settings, and documentation according to established patterns.
3032
31-
By default, cclint scans the entire project and reports on all validation issues.
32-
Use specialized commands to focus on specific file types.
33+
USAGE MODES:
34+
35+
Full scan (default):
36+
cclint Lint all component types
37+
cclint agents Lint only agents
38+
cclint commands Lint only commands
39+
40+
Single-file mode:
41+
cclint ./agents/foo.md Lint a specific file
42+
cclint path/to/file.md Lint by path
43+
cclint a.md b.md c.md Lint multiple files
44+
45+
Explicit file mode (for edge cases):
46+
cclint --file agents Lint a file literally named "agents"
47+
cclint --type agent x.md Override type detection
48+
49+
EXAMPLES:
50+
51+
# Lint a single agent
52+
cclint ./agents/my-agent.md
53+
54+
# Lint multiple files
55+
cclint ./agents/a.md ./commands/b.md
56+
57+
# Force type for file outside standard path
58+
cclint --type skill ./custom/methodology.md
3359
3460
⚠️ NOTE: cclint is a work in progress. Its suggestions should be validated:
3561
• Cross-reference with official docs: docs.anthropic.com, docs.claude.com
3662
• Clear violations (fake flags, >220 lines agents) are reliable
3763
• Style suggestions should be verified against official documentation`,
64+
Args: cobra.ArbitraryArgs,
3865
Run: func(cmd *cobra.Command, args []string) {
39-
if err := runLint(); err != nil {
40-
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
41-
os.Exit(1)
66+
// Determine mode: single-file vs full scan
67+
filesToLint := collectFilesToLint(args)
68+
69+
if len(filesToLint) > 0 {
70+
// Single-file mode
71+
if err := runSingleFileLint(filesToLint); err != nil {
72+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
73+
os.Exit(2) // Exit 2 for invocation errors
74+
}
75+
} else {
76+
// Full scan mode
77+
if err := runLint(); err != nil {
78+
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
79+
os.Exit(1)
80+
}
4281
}
4382
},
4483
}
@@ -52,6 +91,7 @@ func Execute() {
5291
func init() {
5392
cobra.OnInitialize(initConfig)
5493

94+
// Existing flags
5595
rootCmd.PersistentFlags().StringVarP(&rootPath, "root", "r", "", "Project root directory (auto-detected if not specified)")
5696
rootCmd.PersistentFlags().BoolVarP(&quiet, "quiet", "q", false, "Suppress non-essential output")
5797
rootCmd.PersistentFlags().BoolVarP(&verbose, "verbose", "v", false, "Enable verbose output")
@@ -61,6 +101,11 @@ func init() {
61101
rootCmd.PersistentFlags().StringVarP(&outputFile, "output", "o", "", "Output file for reports (requires --format)")
62102
rootCmd.PersistentFlags().StringVarP(&failOn, "fail-on", "", "error", "Fail build on specified level (error|warning|suggestion)")
63103

104+
// Single-file mode flags
105+
rootCmd.Flags().StringArrayVar(&fileFlag, "file", nil, "Explicit file path(s) to lint (use for files with subcommand names)")
106+
rootCmd.Flags().StringVarP(&typeFlag, "type", "t", "", "Force component type (agent|command|skill|settings|context|plugin)")
107+
108+
// Viper bindings
64109
_ = viper.BindPFlag("root", rootCmd.PersistentFlags().Lookup("root"))
65110
_ = viper.BindPFlag("quiet", rootCmd.PersistentFlags().Lookup("quiet"))
66111
_ = viper.BindPFlag("verbose", rootCmd.PersistentFlags().Lookup("verbose"))
@@ -147,5 +192,80 @@ func runLint() error {
147192
os.Exit(1)
148193
}
149194

195+
return nil
196+
}
197+
198+
// collectFilesToLint determines which files to lint based on args and flags.
199+
//
200+
// Priority:
201+
// 1. --file flag (explicit files, bypasses subcommand detection)
202+
// 2. Args that look like file paths
203+
// 3. Empty (full scan mode)
204+
//
205+
// Known subcommands (agents, commands, etc.) are NOT treated as file paths
206+
// unless --file flag is used.
207+
func collectFilesToLint(args []string) []string {
208+
var files []string
209+
210+
// 1. --file flag takes precedence (explicit file mode)
211+
if len(fileFlag) > 0 {
212+
return fileFlag
213+
}
214+
215+
// 2. Check args for file paths
216+
for _, arg := range args {
217+
// Skip known subcommands (they'll be handled by Cobra)
218+
if cli.IsKnownSubcommand(arg) {
219+
continue
220+
}
221+
222+
// Check if it looks like a file path
223+
if cli.LooksLikePath(arg) {
224+
files = append(files, arg)
225+
}
226+
}
227+
228+
return files
229+
}
230+
231+
// runSingleFileLint lints specific files and outputs results.
232+
//
233+
// Exit codes:
234+
// - 0: All files passed (no errors)
235+
// - 1: One or more files had lint errors
236+
// - 2: Invocation error (file not found, invalid type, etc.)
237+
func runSingleFileLint(files []string) error {
238+
// Load configuration for output settings
239+
cfg, err := config.LoadConfig(rootPath)
240+
if err != nil {
241+
return fmt.Errorf("error loading configuration: %w", err)
242+
}
243+
244+
// Override config with flag values
245+
cfg.Quiet = quiet
246+
cfg.Verbose = verbose
247+
248+
// Lint files
249+
summary, err := cli.LintFiles(files, rootPath, typeFlag, cfg.Quiet, cfg.Verbose)
250+
if err != nil {
251+
return err
252+
}
253+
254+
// Output results
255+
outputter := outputters.NewOutputter(cfg)
256+
if err := outputter.Format(summary, cfg.Format); err != nil {
257+
return fmt.Errorf("error formatting output: %w", err)
258+
}
259+
260+
// Print validation reminder (unless quiet mode)
261+
if !cfg.Quiet {
262+
fmt.Println("\n⚠️ Validate suggestions against docs.anthropic.com or docs.claude.com")
263+
}
264+
265+
// Exit with error if any file had errors
266+
if summary.TotalErrors > 0 {
267+
os.Exit(1)
268+
}
269+
150270
return nil
151271
}

0 commit comments

Comments
 (0)