diff --git a/internal/assets/doc.go b/internal/assets/doc.go new file mode 100644 index 00000000..822eaacf --- /dev/null +++ b/internal/assets/doc.go @@ -0,0 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package assets provides embedded assets for ctx including .context/ +// templates and Claude Code plugin files. +package assets diff --git a/internal/assets/embed.go b/internal/assets/embed.go index 15805f1a..81dd96fb 100644 --- a/internal/assets/embed.go +++ b/internal/assets/embed.go @@ -12,6 +12,7 @@ package assets import ( "embed" "encoding/json" + "github.com/ActiveMemory/ctx/internal/config" "strings" "sync" ) @@ -330,7 +331,7 @@ func ListHookVariants(hook string) ([]string, error) { // - []byte: Document content from why/ // - error: Non-nil if the file is not found or read fails func WhyDoc(name string) ([]byte, error) { - return FS.ReadFile("why/" + name + ".md") + return FS.ReadFile("why/" + name + config.ExtMarkdown) } // ListWhyDocs returns available "why" document names (without extension). @@ -348,7 +349,7 @@ func ListWhyDocs() ([]string, error) { for _, entry := range entries { if !entry.IsDir() { name := entry.Name() - if len(name) > 3 && name[len(name)-3:] == ".md" { + if len(name) > 3 && name[len(name)-3:] == config.ExtMarkdown { names = append(names, name[:len(name)-3]) } } @@ -378,7 +379,7 @@ var ( // Lines are trimmed; empty lines and lines starting with '#' are skipped. func parsePermissions(data []byte) []string { var result []string - for _, line := range strings.Split(string(data), "\n") { + for _, line := range strings.Split(string(data), config.NewlineLF) { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue diff --git a/internal/bootstrap/bootstrap.go b/internal/bootstrap/bootstrap.go index 018ac976..ae59bb37 100644 --- a/internal/bootstrap/bootstrap.go +++ b/internal/bootstrap/bootstrap.go @@ -22,8 +22,8 @@ import ( "github.com/ActiveMemory/ctx/internal/cli/agent" "github.com/ActiveMemory/ctx/internal/cli/changes" "github.com/ActiveMemory/ctx/internal/cli/compact" - cliconfig "github.com/ActiveMemory/ctx/internal/cli/config" "github.com/ActiveMemory/ctx/internal/cli/complete" + cliconfig "github.com/ActiveMemory/ctx/internal/cli/config" "github.com/ActiveMemory/ctx/internal/cli/decision" "github.com/ActiveMemory/ctx/internal/cli/deps" "github.com/ActiveMemory/ctx/internal/cli/doctor" @@ -38,8 +38,8 @@ import ( "github.com/ActiveMemory/ctx/internal/cli/notify" "github.com/ActiveMemory/ctx/internal/cli/pad" "github.com/ActiveMemory/ctx/internal/cli/pause" - "github.com/ActiveMemory/ctx/internal/cli/prompt" "github.com/ActiveMemory/ctx/internal/cli/permissions" + "github.com/ActiveMemory/ctx/internal/cli/prompt" "github.com/ActiveMemory/ctx/internal/cli/recall" "github.com/ActiveMemory/ctx/internal/cli/reindex" "github.com/ActiveMemory/ctx/internal/cli/remind" diff --git a/internal/bootstrap/doc.go b/internal/bootstrap/doc.go new file mode 100644 index 00000000..e8d0b17b --- /dev/null +++ b/internal/bootstrap/doc.go @@ -0,0 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package bootstrap initializes the ctx CLI application and registers +// all subcommands. +package bootstrap diff --git a/internal/cli/add/run.go b/internal/cli/add/run.go index 106e53a9..a6845847 100644 --- a/internal/cli/add/run.go +++ b/internal/cli/add/run.go @@ -7,6 +7,7 @@ package add import ( + "fmt" "os" "path/filepath" "strings" @@ -201,7 +202,7 @@ func runAdd(cmd *cobra.Command, args []string, flags addConfig) error { } green := color.New(color.FgGreen).SprintFunc() - cmd.Printf("%s Added to %s\n", green("✓"), fName) + cmd.Println(fmt.Sprintf("%s Added to %s", green("✓"), fName)) return nil } diff --git a/internal/cli/changes/detect.go b/internal/cli/changes/detect.go index 61e74717..9cb8603e 100644 --- a/internal/cli/changes/detect.go +++ b/internal/cli/changes/detect.go @@ -114,7 +114,7 @@ func detectFromEvents() (time.Time, bool) { return time.Time{}, false } - lines := strings.Split(strings.TrimSpace(string(data)), "\n") + lines := strings.Split(strings.TrimSpace(string(data)), config.NewlineLF) // Scan in reverse for last context-load-gate event. for i := len(lines) - 1; i >= 0; i-- { line := lines[i] diff --git a/internal/cli/changes/format.go b/internal/cli/changes/format.go index 74ad9f44..a7ed6b5b 100644 --- a/internal/cli/changes/format.go +++ b/internal/cli/changes/format.go @@ -8,6 +8,7 @@ package changes import ( "fmt" + "github.com/ActiveMemory/ctx/internal/config" "strings" ) @@ -24,7 +25,7 @@ func RenderChanges(refLabel string, ctxChanges []ContextChange, code CodeSummary b.WriteString(fmt.Sprintf("- `%s` — modified %s\n", c.Name, c.ModTime.Format("2006-01-02 15:04"))) } - b.WriteString("\n") + b.WriteString(config.NewlineLF) } if code.CommitCount > 0 { @@ -42,7 +43,7 @@ func RenderChanges(refLabel string, ctxChanges []ContextChange, code CodeSummary b.WriteString(fmt.Sprintf("- **Authors**: %s\n", strings.Join(code.Authors, ", "))) } - b.WriteString("\n") + b.WriteString(config.NewlineLF) } if len(ctxChanges) == 0 && code.CommitCount == 0 { @@ -77,5 +78,5 @@ func RenderChangesForHook(refLabel string, ctxChanges []ContextChange, code Code return "" } - return "Changes since last session: " + strings.Join(parts, ". ") + "\n" + return "Changes since last session: " + strings.Join(parts, ". ") + config.NewlineLF } diff --git a/internal/cli/changes/scan.go b/internal/cli/changes/scan.go index d97bce89..db36ccc0 100644 --- a/internal/cli/changes/scan.go +++ b/internal/cli/changes/scan.go @@ -7,6 +7,7 @@ package changes import ( + "github.com/ActiveMemory/ctx/internal/config" "os" "os/exec" "sort" @@ -40,7 +41,7 @@ func FindContextChanges(refTime time.Time) ([]ContextChange, error) { var changes []ContextChange for _, e := range entries { - if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") { + if e.IsDir() || !strings.HasSuffix(e.Name(), config.ExtMarkdown) { continue } info, infoErr := e.Info() @@ -78,7 +79,7 @@ func SummarizeCodeChanges(refTime time.Time) (CodeSummary, error) { if lines == "" { return summary, nil } - commitLines := strings.Split(lines, "\n") + commitLines := strings.Split(lines, config.NewlineLF) summary.CommitCount = len(commitLines) // Latest commit message (first line of oneline output). @@ -110,7 +111,7 @@ func SummarizeCodeChanges(refTime time.Time) (CodeSummary, error) { // uniqueTopDirs extracts unique top-level directories from file paths. func uniqueTopDirs(output string) []string { seen := make(map[string]bool) - for _, line := range strings.Split(strings.TrimSpace(output), "\n") { + for _, line := range strings.Split(strings.TrimSpace(output), config.NewlineLF) { line = strings.TrimSpace(line) if line == "" { continue @@ -133,7 +134,7 @@ func uniqueTopDirs(output string) []string { // uniqueLines returns unique non-empty lines from output. func uniqueLines(output string) []string { seen := make(map[string]bool) - for _, line := range strings.Split(strings.TrimSpace(output), "\n") { + for _, line := range strings.Split(strings.TrimSpace(output), config.NewlineLF) { line = strings.TrimSpace(line) if line != "" { seen[line] = true diff --git a/internal/cli/config/switch.go b/internal/cli/config/switch.go index 96187fc7..e072a189 100644 --- a/internal/cli/config/switch.go +++ b/internal/cli/config/switch.go @@ -56,7 +56,7 @@ func statusCmd() *cobra.Command { Use: "status", Short: "Show active .ctxrc profile", Annotations: map[string]string{internalConfig.AnnotationSkipInit: ""}, - Args: cobra.NoArgs, + Args: cobra.NoArgs, RunE: func(cmd *cobra.Command, _ []string) error { root, rootErr := gitRoot() if rootErr != nil { @@ -99,7 +99,7 @@ func runSwitch(cmd *cobra.Command, root string, args []string) error { func switchTo(cmd *cobra.Command, root, profile string) error { current := detectProfile(root) if current == profile { - cmd.Printf("already on %s profile\n", profile) + cmd.Println(fmt.Sprintf("already on %s profile", profile)) return nil } @@ -115,9 +115,9 @@ func switchTo(cmd *cobra.Command, root, profile string) error { } if current == "" { - cmd.Printf("created %s from %s profile\n", fileCtxRC, profile) + cmd.Println(fmt.Sprintf("created %s from %s profile", fileCtxRC, profile)) } else { - cmd.Printf("switched to %s profile\n", profile) + cmd.Println(fmt.Sprintf("switched to %s profile", profile)) } return nil } @@ -130,7 +130,7 @@ func runStatus(cmd *cobra.Command, root string) error { case profileBase: cmd.Println("active: base (defaults)") default: - cmd.Printf("active: none (%s does not exist)\n", fileCtxRC) + cmd.Println(fmt.Sprintf("active: none (%s does not exist)", fileCtxRC)) } return nil } @@ -143,7 +143,7 @@ func detectProfile(root string) string { return "" } - for _, line := range strings.Split(string(data), "\n") { + for _, line := range strings.Split(string(data), internalConfig.NewlineLF) { if strings.HasPrefix(strings.TrimSpace(line), "notify:") { return profileDev } diff --git a/internal/cli/decision/doc.go b/internal/cli/decision/doc.go new file mode 100644 index 00000000..2fc82701 --- /dev/null +++ b/internal/cli/decision/doc.go @@ -0,0 +1,8 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package decision manages DECISIONS.md file and its quick-reference index. +package decision diff --git a/internal/cli/deps/format.go b/internal/cli/deps/format.go index 83d609b8..8e6d9b01 100644 --- a/internal/cli/deps/format.go +++ b/internal/cli/deps/format.go @@ -9,6 +9,7 @@ package deps import ( "encoding/json" "fmt" + "github.com/ActiveMemory/ctx/internal/config" "sort" "strings" ) @@ -57,7 +58,7 @@ func renderTable(graph map[string][]string) string { // renderJSON produces a machine-readable JSON adjacency list. func renderJSON(graph map[string][]string) string { data, _ := json.MarshalIndent(graph, "", " ") - return string(data) + "\n" + return string(data) + config.NewlineLF } // sortedKeys returns the keys of a map sorted alphabetically. diff --git a/internal/cli/deps/python.go b/internal/cli/deps/python.go index b8b7adfd..10e6aa86 100644 --- a/internal/cli/deps/python.go +++ b/internal/cli/deps/python.go @@ -8,6 +8,7 @@ package deps import ( "bufio" + "github.com/ActiveMemory/ctx/internal/config" "os" "sort" "strings" @@ -148,7 +149,7 @@ func buildPyprojectGraph(includeDevDeps bool) (map[string][]string, error) { // parsePyprojectDeps extracts dependency names from a TOML array section. // Looks for [project.dependencies], [tool.poetry.dependencies], etc. func parsePyprojectDeps(content string, sectionSuffix string) []string { - lines := strings.Split(content, "\n") + lines := strings.Split(content, config.NewlineLF) var deps []string inSection := false inArray := false diff --git a/internal/cli/deps/rust.go b/internal/cli/deps/rust.go index 4f41ff31..5e5f87f8 100644 --- a/internal/cli/deps/rust.go +++ b/internal/cli/deps/rust.go @@ -14,10 +14,12 @@ import ( "sort" ) +const rustEcosystem = "rust" + // rustBuilder implements GraphBuilder for Rust projects. type rustBuilder struct{} -func (r *rustBuilder) Name() string { return "rust" } +func (r *rustBuilder) Name() string { return rustEcosystem } func (r *rustBuilder) Detect() bool { _, err := os.Stat("Cargo.toml") @@ -49,7 +51,7 @@ type cargoPackage struct { // cargoDep represents a dependency entry in cargo metadata. type cargoDep struct { - Name string `json:"name"` + Name string `json:"name"` Kind *string `json:"kind"` } diff --git a/internal/cli/doctor/doc.go b/internal/cli/doctor/doc.go new file mode 100644 index 00000000..dcde95a1 --- /dev/null +++ b/internal/cli/doctor/doc.go @@ -0,0 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package doctor performs structural health checks for context, hooks, +// and configuration. +package doctor diff --git a/internal/cli/doctor/doctor.go b/internal/cli/doctor/doctor.go index f6e6f6b4..00ffbd74 100644 --- a/internal/cli/doctor/doctor.go +++ b/internal/cli/doctor/doctor.go @@ -635,12 +635,12 @@ func outputDoctorHuman(cmd *cobra.Command, report *Report) error { cmd.Println(cat) for _, r := range results { icon := statusIcon(r.Status) - cmd.Printf(" %s %s\n", icon, r.Message) + cmd.Println(fmt.Sprintf(" %s %s", icon, r.Message)) } cmd.Println() } - cmd.Printf("Summary: %d warnings, %d errors\n", report.Warnings, report.Errors) + cmd.Println(fmt.Sprintf("Summary: %d warnings, %d errors", report.Warnings, report.Errors)) return nil } diff --git a/internal/cli/guide/guide_test.go b/internal/cli/guide/guide_test.go index 081eab35..d548b924 100644 --- a/internal/cli/guide/guide_test.go +++ b/internal/cli/guide/guide_test.go @@ -113,7 +113,7 @@ func TestParseSkillFrontmatter(t *testing.T) { wantErr bool }{ { - name: "valid frontmatter", + name: "valid frontmatter", input: "---\nname: ctx-test\ndescription: \"A test skill.\"\n---\nBody text.", want: skillMeta{Name: "ctx-test", Description: "A test skill."}, }, diff --git a/internal/cli/guide/skills.go b/internal/cli/guide/skills.go index a028a00d..8a30fffa 100644 --- a/internal/cli/guide/skills.go +++ b/internal/cli/guide/skills.go @@ -9,6 +9,7 @@ package guide import ( "bytes" "fmt" + "github.com/ActiveMemory/ctx/internal/config" "strings" "github.com/spf13/cobra" @@ -32,11 +33,11 @@ func parseSkillFrontmatter(content []byte) (skillMeta, error) { const sep = "---" text := string(content) - if !strings.HasPrefix(text, sep+"\n") { + if !strings.HasPrefix(text, sep+config.NewlineLF) { return skillMeta{}, nil } - end := strings.Index(text[4:], "\n"+sep) + end := strings.Index(text[4:], config.NewlineLF+sep) if end < 0 { return skillMeta{}, nil } diff --git a/internal/cli/hook/run.go b/internal/cli/hook/run.go index f19e0e28..1bd4c1ba 100644 --- a/internal/cli/hook/run.go +++ b/internal/cli/hook/run.go @@ -248,7 +248,7 @@ Run 'ctx agent' for AI-ready context packet. cmd.Println(green("```")) default: - cmd.Printf("Unknown tool: %s\n\n", tool) + cmd.Println(fmt.Sprintf("Unknown tool: %s\n", tool)) cmd.Println("Supported tools:") cmd.Println(" claude-code - Anthropic's Claude Code CLI (use plugin instead)") cmd.Println(" cursor - Cursor IDE") diff --git a/internal/cli/initialize/claude.go b/internal/cli/initialize/claude.go index 6230c894..aec45c69 100644 --- a/internal/cli/initialize/claude.go +++ b/internal/cli/initialize/claude.go @@ -57,7 +57,7 @@ func handleClaudeMd(cmd *cobra.Command, force, autoMerge bool) error { ); err != nil { return fmt.Errorf("failed to write %s: %w", config.FileClaudeMd, err) } - cmd.Printf(" %s %s\n", green("✓"), config.FileClaudeMd) + cmd.Println(fmt.Sprintf(" %s %s", green("✓"), config.FileClaudeMd)) return nil } @@ -68,10 +68,10 @@ func handleClaudeMd(cmd *cobra.Command, force, autoMerge bool) error { if hasCtxMarkers { // Already has ctx content if !force { - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s %s (ctx content exists, skipped)\n", yellow("○"), config.FileClaudeMd, - ) + )) return nil } // Force update: replace the existing ctx section @@ -81,9 +81,9 @@ func handleClaudeMd(cmd *cobra.Command, force, autoMerge bool) error { // No ctx markers: need to merge if !autoMerge { // Prompt user - cmd.Printf( + cmd.Println(fmt.Sprintf( "\n%s exists but has no ctx content.\n", config.FileClaudeMd, - ) + )) cmd.Println( "Would you like to append ctx context management instructions?", ) @@ -95,7 +95,7 @@ func handleClaudeMd(cmd *cobra.Command, force, autoMerge bool) error { } response = strings.TrimSpace(strings.ToLower(response)) if response != "y" && response != "yes" { //nolint:goconst // trivial user input check - cmd.Printf(" %s %s (skipped)\n", yellow("○"), config.FileClaudeMd) + cmd.Println(fmt.Sprintf(" %s %s (skipped)", yellow("○"), config.FileClaudeMd)) return nil } } @@ -106,7 +106,7 @@ func handleClaudeMd(cmd *cobra.Command, force, autoMerge bool) error { if err := os.WriteFile(backupName, existingContent, config.PermFile); err != nil { return fmt.Errorf("failed to create backup %s: %w", backupName, err) } - cmd.Printf(" %s %s (backup)\n", green("✓"), backupName) + cmd.Println(fmt.Sprintf(" %s %s (backup)", green("✓"), backupName)) // Find the best insertion point (after the H1 title, or at the top) insertPos := findInsertionPoint(existingStr) @@ -115,11 +115,11 @@ func handleClaudeMd(cmd *cobra.Command, force, autoMerge bool) error { var mergedContent string if insertPos == 0 { // Insert at top - mergedContent = string(templateContent) + "\n" + existingStr + mergedContent = string(templateContent) + config.NewlineLF + existingStr } else { // Insert after H1 heading - mergedContent = existingStr[:insertPos] + "\n" + - string(templateContent) + "\n" + existingStr[insertPos:] + mergedContent = existingStr[:insertPos] + config.NewlineLF + + string(templateContent) + config.NewlineLF + existingStr[insertPos:] } if err := os.WriteFile( @@ -127,7 +127,7 @@ func handleClaudeMd(cmd *cobra.Command, force, autoMerge bool) error { return fmt.Errorf( "failed to write merged %s: %w", config.FileClaudeMd, err) } - cmd.Printf(" %s %s (merged)\n", green("✓"), config.FileClaudeMd) + cmd.Println(fmt.Sprintf(" %s %s (merged)", green("✓"), config.FileClaudeMd)) return nil } diff --git a/internal/cli/initialize/dirs.go b/internal/cli/initialize/dirs.go index e0e9344a..47eb1901 100644 --- a/internal/cli/initialize/dirs.go +++ b/internal/cli/initialize/dirs.go @@ -39,8 +39,8 @@ func createProjectDirs(cmd *cobra.Command) error { for _, dir := range projectDirs { if _, statErr := os.Stat(dir); statErr == nil { - cmd.Printf(" %s %s/ (exists, skipped)\n", - color.YellowString("○"), dir) + cmd.Println(fmt.Sprintf(" %s %s/ (exists, skipped)", + color.YellowString("○"), dir)) continue } @@ -59,7 +59,7 @@ func createProjectDirs(cmd *cobra.Command) error { return fmt.Errorf("failed to write %s: %w", readmePath, writeErr) } - cmd.Printf(" %s %s/\n", green("✓"), dir) + cmd.Println(fmt.Sprintf(" %s %s/", green("✓"), dir)) } return nil diff --git a/internal/cli/initialize/fs.go b/internal/cli/initialize/fs.go index 06117c19..6681f8ac 100644 --- a/internal/cli/initialize/fs.go +++ b/internal/cli/initialize/fs.go @@ -33,7 +33,7 @@ import ( // Returns: // - int: Byte position where ctx content should be inserted func findInsertionPoint(content string) int { - lines := strings.Split(content, "\n") + lines := strings.Split(content, config.NewlineLF) pos := 0 for i, line := range lines { @@ -135,16 +135,16 @@ func updateCtxSection( if err := os.WriteFile(backupName, []byte(existing), config.PermFile); err != nil { return fmt.Errorf("failed to create backup: %w", err) } - cmd.Printf(" %s %s (backup)\n", green("✓"), backupName) + cmd.Println(fmt.Sprintf(" %s %s (backup)", green("✓"), backupName)) if err := os.WriteFile( config.FileClaudeMd, []byte(newContent), config.PermFile, ); err != nil { return fmt.Errorf("failed to update %s: %w", config.FileClaudeMd, err) } - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s %s (updated ctx section)\n", green("✓"), config.FileClaudeMd, - ) + )) return nil } diff --git a/internal/cli/initialize/hook.go b/internal/cli/initialize/hook.go index 165dc50e..67c29ae0 100644 --- a/internal/cli/initialize/hook.go +++ b/internal/cli/initialize/hook.go @@ -59,9 +59,9 @@ func mergeSettingsPermissions(cmd *cobra.Command) error { denyDeduped := deduplicatePermissions(&settings.Permissions.Deny) if !allowModified && !denyModified && !allowDeduped && !denyDeduped { - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s %s (no changes needed)\n", yellow("○"), config.FileSettings, - ) + )) return nil } @@ -88,18 +88,18 @@ func mergeSettingsPermissions(cmd *cobra.Command) error { merged := allowModified || denyModified switch { case merged && deduped: - cmd.Printf(" %s %s (added ctx permissions, removed duplicates)\n", green("✓"), config.FileSettings) + cmd.Println(fmt.Sprintf(" %s %s (added ctx permissions, removed duplicates)", green("✓"), config.FileSettings)) case deduped: - cmd.Printf(" %s %s (removed duplicate permissions)\n", green("✓"), config.FileSettings) + cmd.Println(fmt.Sprintf(" %s %s (removed duplicate permissions)", green("✓"), config.FileSettings)) case allowModified && denyModified: - cmd.Printf(" %s %s (added ctx allow + deny permissions)\n", green("✓"), config.FileSettings) + cmd.Println(fmt.Sprintf(" %s %s (added ctx allow + deny permissions)", green("✓"), config.FileSettings)) case denyModified: - cmd.Printf(" %s %s (added ctx deny permissions)\n", green("✓"), config.FileSettings) + cmd.Println(fmt.Sprintf(" %s %s (added ctx deny permissions)", green("✓"), config.FileSettings)) default: - cmd.Printf(" %s %s (added ctx permissions)\n", green("✓"), config.FileSettings) + cmd.Println(fmt.Sprintf(" %s %s (added ctx permissions)", green("✓"), config.FileSettings)) } } else { - cmd.Printf(" %s %s\n", green("✓"), config.FileSettings) + cmd.Println(fmt.Sprintf(" %s %s", green("✓"), config.FileSettings)) } return nil diff --git a/internal/cli/initialize/makefile.go b/internal/cli/initialize/makefile.go index ee608851..356aad63 100644 --- a/internal/cli/initialize/makefile.go +++ b/internal/cli/initialize/makefile.go @@ -53,47 +53,47 @@ func handleMakefileCtx(cmd *cobra.Command) error { "failed to write %s: %w", config.FileMakefileCtx, err, ) } - cmd.Printf(" %s %s\n", green("✓"), config.FileMakefileCtx) + cmd.Println(fmt.Sprintf(" %s %s", green("✓"), config.FileMakefileCtx)) // Ensure the user's Makefile includes Makefile.ctx existing, err := os.ReadFile("Makefile") if err != nil { // No Makefile — create a minimal one - minimal := includeDirective + "\n" + minimal := includeDirective + config.NewlineLF if err := os.WriteFile( "Makefile", []byte(minimal), config.PermFile, ); err != nil { return fmt.Errorf("failed to create Makefile: %w", err) } - cmd.Printf(" %s Makefile (created with ctx include)\n", green("✓")) + cmd.Println(fmt.Sprintf(" %s Makefile (created with ctx include)", green("✓"))) return nil } // Makefile exists — check if it already includes Makefile.ctx if strings.Contains(string(existing), includeDirective) { - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s Makefile (already includes %s)\n", yellow("○"), config.FileMakefileCtx, - ) + )) return nil } // Append the include directive amended := string(existing) - if !strings.HasSuffix(amended, "\n") { - amended += "\n" + if !strings.HasSuffix(amended, config.NewlineLF) { + amended += config.NewlineLF } - amended += "\n" + includeDirective + "\n" + amended += config.NewlineLF + includeDirective + config.NewlineLF if err := os.WriteFile( "Makefile", []byte(amended), config.PermFile, ); err != nil { return fmt.Errorf("failed to amend Makefile: %w", err) } - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s Makefile (appended %s include)\n", green("✓"), config.FileMakefileCtx, - ) + )) return nil } diff --git a/internal/cli/initialize/plan.go b/internal/cli/initialize/plan.go index 20e9f8c6..70044511 100644 --- a/internal/cli/initialize/plan.go +++ b/internal/cli/initialize/plan.go @@ -60,7 +60,7 @@ func handleImplementationPlan(cmd *cobra.Command, force, autoMerge bool) error { return fmt.Errorf( "failed to write %s: %w", config.FileImplementationPlan, err) } - cmd.Printf(" %s %s\n", green("✓"), config.FileImplementationPlan) + cmd.Println(fmt.Sprintf(" %s %s", green("✓"), config.FileImplementationPlan)) return nil } @@ -71,10 +71,10 @@ func handleImplementationPlan(cmd *cobra.Command, force, autoMerge bool) error { if hasCtxMarkers { // Already has ctx content if !force { - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s %s (ctx content exists, skipped)\n", yellow("○"), config.FileImplementationPlan, - ) + )) return nil } // Force update: replace the existing ctx section @@ -84,10 +84,10 @@ func handleImplementationPlan(cmd *cobra.Command, force, autoMerge bool) error { // No ctx markers: need to merge if !autoMerge { // Prompt user - cmd.Printf( + cmd.Println(fmt.Sprintf( "\n%s exists but has no ctx content.\n", config.FileImplementationPlan, - ) + )) cmd.Println( "Would you like to merge ctx implementation plan template?", ) @@ -99,8 +99,8 @@ func handleImplementationPlan(cmd *cobra.Command, force, autoMerge bool) error { } response = strings.TrimSpace(strings.ToLower(response)) if response != "y" && response != "yes" { //nolint:goconst // trivial user input check - cmd.Printf( - " %s %s (skipped)\n", yellow("○"), config.FileImplementationPlan) + cmd.Println(fmt.Sprintf( + " %s %s (skipped)\n", yellow("○"), config.FileImplementationPlan)) return nil } } @@ -112,7 +112,7 @@ func handleImplementationPlan(cmd *cobra.Command, force, autoMerge bool) error { if err := os.WriteFile(backupName, existingContent, config.PermFile); err != nil { return fmt.Errorf("failed to create backup %s: %w", backupName, err) } - cmd.Printf(" %s %s (backup)\n", green("✓"), backupName) + cmd.Println(fmt.Sprintf(" %s %s (backup)", green("✓"), backupName)) // Find the best insertion point (after the H1 title, or at the top) insertPos := findInsertionPoint(existingStr) @@ -121,11 +121,11 @@ func handleImplementationPlan(cmd *cobra.Command, force, autoMerge bool) error { var mergedContent string if insertPos == 0 { // Insert at top - mergedContent = string(templateContent) + "\n" + existingStr + mergedContent = string(templateContent) + config.NewlineLF + existingStr } else { // Insert after H1 heading - mergedContent = existingStr[:insertPos] + "\n" + - string(templateContent) + "\n" + existingStr[insertPos:] + mergedContent = existingStr[:insertPos] + config.NewlineLF + + string(templateContent) + config.NewlineLF + existingStr[insertPos:] } if err := os.WriteFile( @@ -133,7 +133,7 @@ func handleImplementationPlan(cmd *cobra.Command, force, autoMerge bool) error { return fmt.Errorf( "failed to write merged %s: %w", config.FileImplementationPlan, err) } - cmd.Printf(" %s %s (merged)\n", green("✓"), config.FileImplementationPlan) + cmd.Println(fmt.Sprintf(" %s %s (merged)", green("✓"), config.FileImplementationPlan)) return nil } @@ -187,7 +187,7 @@ func updatePlanSection( if err := os.WriteFile(backupName, []byte(existing), config.PermFile); err != nil { return fmt.Errorf("failed to create backup: %w", err) } - cmd.Printf(" %s %s (backup)\n", green("✓"), backupName) + cmd.Println(fmt.Sprintf(" %s %s (backup)", green("✓"), backupName)) if err := os.WriteFile( config.FileImplementationPlan, []byte(newContent), config.PermFile, @@ -195,10 +195,10 @@ func updatePlanSection( return fmt.Errorf( "failed to update %s: %w", config.FileImplementationPlan, err) } - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s %s (updated plan section)\n", green("✓"), config.FileImplementationPlan, - ) + )) return nil } diff --git a/internal/cli/initialize/plugin.go b/internal/cli/initialize/plugin.go index 518bc6cd..b9e598ca 100644 --- a/internal/cli/initialize/plugin.go +++ b/internal/cli/initialize/plugin.go @@ -57,10 +57,10 @@ func enablePluginGlobally(cmd *cobra.Command) error { installedPath := filepath.Join(claudeDir, config.FileInstalledPlugins) installedData, readErr := os.ReadFile(installedPath) //nolint:gosec // G304: path from os.UserHomeDir if readErr != nil { - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s Plugin enablement skipped (plugin not installed)\n", yellow("○"), - ) + )) return nil } @@ -70,10 +70,10 @@ func enablePluginGlobally(cmd *cobra.Command) error { } if _, found := installed.Plugins[config.PluginID]; !found { - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s Plugin enablement skipped (plugin not installed)\n", yellow("○"), - ) + )) return nil } @@ -99,10 +99,10 @@ func enablePluginGlobally(cmd *cobra.Command) error { var enabled map[string]bool if parseErr := json.Unmarshal(raw, &enabled); parseErr == nil { if enabled[config.PluginID] { - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s Plugin already enabled globally\n", yellow("○"), - ) + )) return nil } } @@ -139,7 +139,7 @@ func enablePluginGlobally(cmd *cobra.Command) error { return fmt.Errorf("failed to write %s: %w", settingsPath, writeErr) } - cmd.Printf(" %s Plugin enabled globally in %s\n", green("✓"), settingsPath) + cmd.Println(fmt.Sprintf(" %s Plugin enabled globally in %s", green("✓"), settingsPath)) return nil } diff --git a/internal/cli/initialize/prompt.go b/internal/cli/initialize/prompt.go index b2709bb6..bc1cb800 100644 --- a/internal/cli/initialize/prompt.go +++ b/internal/cli/initialize/prompt.go @@ -75,7 +75,7 @@ func handlePromptMd(cmd *cobra.Command, force, autoMerge, ralph bool) error { if ralph { mode = " (ralph mode)" } - cmd.Printf(" %s %s%s\n", green("✓"), config.FilePromptMd, mode) + cmd.Println(fmt.Sprintf(" %s %s%s", green("✓"), config.FilePromptMd, mode)) return nil } @@ -86,10 +86,10 @@ func handlePromptMd(cmd *cobra.Command, force, autoMerge, ralph bool) error { if hasCtxMarkers { // Already has ctx content if !force { - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s %s (ctx content exists, skipped)\n", yellow("○"), config.FilePromptMd, - ) + )) return nil } // Force update: replace the existing ctx section @@ -99,9 +99,9 @@ func handlePromptMd(cmd *cobra.Command, force, autoMerge, ralph bool) error { // No ctx markers: need to merge if !autoMerge { // Prompt user - cmd.Printf( + cmd.Println(fmt.Sprintf( "\n%s exists but has no ctx content.\n", config.FilePromptMd, - ) + )) cmd.Println( "Would you like to merge ctx prompt instructions?", ) @@ -113,7 +113,7 @@ func handlePromptMd(cmd *cobra.Command, force, autoMerge, ralph bool) error { } response = strings.TrimSpace(strings.ToLower(response)) if response != "y" && response != "yes" { //nolint:goconst // trivial user input check - cmd.Printf(" %s %s (skipped)\n", yellow("○"), config.FilePromptMd) + cmd.Println(fmt.Sprintf(" %s %s (skipped)", yellow("○"), config.FilePromptMd)) return nil } } @@ -124,7 +124,7 @@ func handlePromptMd(cmd *cobra.Command, force, autoMerge, ralph bool) error { if err := os.WriteFile(backupName, existingContent, config.PermFile); err != nil { return fmt.Errorf("failed to create backup %s: %w", backupName, err) } - cmd.Printf(" %s %s (backup)\n", green("✓"), backupName) + cmd.Println(fmt.Sprintf(" %s %s (backup)", green("✓"), backupName)) // Find the best insertion point (after the H1 title, or at the top) insertPos := findInsertionPoint(existingStr) @@ -133,11 +133,11 @@ func handlePromptMd(cmd *cobra.Command, force, autoMerge, ralph bool) error { var mergedContent string if insertPos == 0 { // Insert at top - mergedContent = string(templateContent) + "\n" + existingStr + mergedContent = string(templateContent) + config.NewlineLF + existingStr } else { // Insert after H1 heading - mergedContent = existingStr[:insertPos] + "\n" + - string(templateContent) + "\n" + existingStr[insertPos:] + mergedContent = existingStr[:insertPos] + config.NewlineLF + + string(templateContent) + config.NewlineLF + existingStr[insertPos:] } if err := os.WriteFile( @@ -145,7 +145,7 @@ func handlePromptMd(cmd *cobra.Command, force, autoMerge, ralph bool) error { return fmt.Errorf( "failed to write merged %s: %w", config.FilePromptMd, err) } - cmd.Printf(" %s %s (merged)\n", green("✓"), config.FilePromptMd) + cmd.Println(fmt.Sprintf(" %s %s (merged)", green("✓"), config.FilePromptMd)) return nil } @@ -198,16 +198,16 @@ func updatePromptSection( if err := os.WriteFile(backupName, []byte(existing), config.PermFile); err != nil { return fmt.Errorf("failed to create backup: %w", err) } - cmd.Printf(" %s %s (backup)\n", green("✓"), backupName) + cmd.Println(fmt.Sprintf(" %s %s (backup)", green("✓"), backupName)) if err := os.WriteFile( config.FilePromptMd, []byte(newContent), config.PermFile, ); err != nil { return fmt.Errorf("failed to update %s: %w", config.FilePromptMd, err) } - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s %s (updated prompt section)\n", green("✓"), config.FilePromptMd, - ) + )) return nil } diff --git a/internal/cli/initialize/prompt_tpl.go b/internal/cli/initialize/prompt_tpl.go index 48d7f175..9b6befbc 100644 --- a/internal/cli/initialize/prompt_tpl.go +++ b/internal/cli/initialize/prompt_tpl.go @@ -50,7 +50,7 @@ func createPromptTemplates( // Check if the file exists and --force not set if _, err := os.Stat(targetPath); err == nil && !force { - cmd.Printf(" %s prompts/%s (exists, skipped)\n", yellow("○"), name) + cmd.Println(fmt.Sprintf(" %s prompts/%s (exists, skipped)", yellow("○"), name)) continue } @@ -63,7 +63,7 @@ func createPromptTemplates( return fmt.Errorf("failed to write %s: %w", targetPath, err) } - cmd.Printf(" %s prompts/%s\n", green("✓"), name) + cmd.Println(fmt.Sprintf(" %s prompts/%s", green("✓"), name)) } return nil diff --git a/internal/cli/initialize/run.go b/internal/cli/initialize/run.go index c3beaa9c..091bf11c 100644 --- a/internal/cli/initialize/run.go +++ b/internal/cli/initialize/run.go @@ -51,7 +51,7 @@ func runInit(cmd *cobra.Command, force, minimal, merge, ralph, noPluginEnable bo if _, err := os.Stat(contextDir); err == nil { if !force && hasEssentialFiles(contextDir) { // Prompt for confirmation - cmd.Printf("%s already exists. Overwrite? [y/N] ", contextDir) + cmd.Print(fmt.Sprintf("%s already exists. Overwrite? [y/N] ", contextDir)) reader := bufio.NewReader(os.Stdin) response, err := reader.ReadString('\n') if err != nil { @@ -89,9 +89,9 @@ func runInit(cmd *cobra.Command, force, minimal, merge, ralph, noPluginEnable bo // Check if the file exists and --force not set if _, err := os.Stat(targetPath); err == nil && !force { - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s %s (exists, skipped)\n", color.YellowString("○"), name, - ) + )) continue } @@ -104,21 +104,21 @@ func runInit(cmd *cobra.Command, force, minimal, merge, ralph, noPluginEnable bo return fmt.Errorf("failed to write %s: %w", targetPath, err) } - cmd.Printf(" %s %s\n", green("✓"), name) + cmd.Println(fmt.Sprintf(" %s %s", green("✓"), name)) } - cmd.Printf("\n%s initialized in %s/\n", green("Context"), contextDir) + cmd.Println(fmt.Sprintf("\n%s initialized in %s/", green("Context"), contextDir)) // Create entry templates in .context/templates/ if err := createEntryTemplates(cmd, contextDir, force); err != nil { // Non-fatal: warn but continue - cmd.Printf(" %s Entry templates: %v\n", color.YellowString("⚠"), err) + cmd.Println(fmt.Sprintf(" %s Entry templates: %v", color.YellowString("⚠"), err)) } // Create prompt templates in .context/prompts/ if err := createPromptTemplates(cmd, contextDir, force); err != nil { // Non-fatal: warn but continue - cmd.Printf(" %s Prompt templates: %v\n", color.YellowString("⚠"), err) + cmd.Println(fmt.Sprintf(" %s Prompt templates: %v", color.YellowString("⚠"), err)) } // Migrate legacy key files and promote to global path. @@ -127,7 +127,7 @@ func runInit(cmd *cobra.Command, force, minimal, merge, ralph, noPluginEnable bo // Set up scratchpad if err := initScratchpad(cmd, contextDir); err != nil { // Non-fatal: warn but continue - cmd.Printf(" %s Scratchpad: %v\n", color.YellowString("⚠"), err) + cmd.Println(fmt.Sprintf(" %s Scratchpad: %v", color.YellowString("⚠"), err)) } // Create project root files @@ -135,53 +135,53 @@ func runInit(cmd *cobra.Command, force, minimal, merge, ralph, noPluginEnable bo // Create specs/ and ideas/ directories with README.md if err := createProjectDirs(cmd); err != nil { - cmd.Printf(" %s Project dirs: %v\n", color.YellowString("⚠"), err) + cmd.Println(fmt.Sprintf(" %s Project dirs: %v", color.YellowString("⚠"), err)) } // Create PROMPT.md (uses ralph template if --ralph flag set) if err := handlePromptMd(cmd, force, merge, ralph); err != nil { // Non-fatal: warn but continue - cmd.Printf(" %s PROMPT.md: %v\n", color.YellowString("⚠"), err) + cmd.Println(fmt.Sprintf(" %s PROMPT.md: %v", color.YellowString("⚠"), err)) } // Create IMPLEMENTATION_PLAN.md if err := handleImplementationPlan(cmd, force, merge); err != nil { // Non-fatal: warn but continue - cmd.Printf( + cmd.Println(fmt.Sprintf( " %s IMPLEMENTATION_PLAN.md: %v\n", color.YellowString("⚠"), err, - ) + )) } // Merge permissions into settings.local.json (no hook scaffolding) cmd.Println("\nSetting up Claude Code permissions...") if err := mergeSettingsPermissions(cmd); err != nil { // Non-fatal: warn but continue - cmd.Printf(" %s Permissions: %v\n", color.YellowString("⚠"), err) + cmd.Println(fmt.Sprintf(" %s Permissions: %v", color.YellowString("⚠"), err)) } // Auto-enable plugin globally unless suppressed if !noPluginEnable { if pluginErr := enablePluginGlobally(cmd); pluginErr != nil { // Non-fatal: warn but continue - cmd.Printf(" %s Plugin enablement: %v\n", color.YellowString("⚠"), pluginErr) + cmd.Println(fmt.Sprintf(" %s Plugin enablement: %v", color.YellowString("⚠"), pluginErr)) } } // Handle CLAUDE.md creation/merge if err := handleClaudeMd(cmd, force, merge); err != nil { // Non-fatal: warn but continue - cmd.Printf(" %s CLAUDE.md: %v\n", color.YellowString("⚠"), err) + cmd.Println(fmt.Sprintf(" %s CLAUDE.md: %v", color.YellowString("⚠"), err)) } // Deploy Makefile.ctx and amend user Makefile if err := handleMakefileCtx(cmd); err != nil { // Non-fatal: warn but continue - cmd.Printf(" %s Makefile: %v\n", color.YellowString("⚠"), err) + cmd.Println(fmt.Sprintf(" %s Makefile: %v", color.YellowString("⚠"), err)) } // Update .gitignore with recommended entries if err := ensureGitignoreEntries(cmd); err != nil { - cmd.Printf(" %s .gitignore: %v\n", color.YellowString("⚠"), err) + cmd.Println(fmt.Sprintf(" %s .gitignore: %v", color.YellowString("⚠"), err)) } cmd.Println("\nNext steps:") @@ -228,9 +228,9 @@ func initScratchpad(cmd *cobra.Command, contextDir string) error { if err := os.WriteFile(mdPath, nil, config.PermFile); err != nil { return fmt.Errorf("failed to create %s: %w", mdPath, err) } - cmd.Printf(" %s %s (plaintext scratchpad)\n", green("✓"), mdPath) + cmd.Println(fmt.Sprintf(" %s %s (plaintext scratchpad)", green("✓"), mdPath)) } else { - cmd.Printf(" %s %s (exists, skipped)\n", yellow("○"), mdPath) + cmd.Println(fmt.Sprintf(" %s %s (exists, skipped)", yellow("○"), mdPath)) } return nil } @@ -241,14 +241,14 @@ func initScratchpad(cmd *cobra.Command, contextDir string) error { // Check if key already exists (idempotent) if _, err := os.Stat(kPath); err == nil { - cmd.Printf(" %s %s (exists, skipped)\n", yellow("○"), kPath) + cmd.Println(fmt.Sprintf(" %s %s (exists, skipped)", yellow("○"), kPath)) return nil } // Warn if encrypted file exists but no key if _, err := os.Stat(encPath); err == nil { - cmd.Printf(" %s Encrypted scratchpad found but no key at %s\n", - yellow("⚠"), kPath) + cmd.Println(fmt.Sprintf(" %s Encrypted scratchpad found but no key at %s", + yellow("⚠"), kPath)) return nil } @@ -266,7 +266,7 @@ func initScratchpad(cmd *cobra.Command, contextDir string) error { if err := crypto.SaveKey(kPath, key); err != nil { return fmt.Errorf("failed to save scratchpad key: %w", err) } - cmd.Printf(" %s Scratchpad key created at %s\n", green("✓"), kPath) + cmd.Println(fmt.Sprintf(" %s Scratchpad key created at %s", green("✓"), kPath)) return nil } @@ -296,7 +296,7 @@ func ensureGitignoreEntries(cmd *cobra.Command) error { // Build set of existing trimmed lines. existing := make(map[string]bool) - for _, line := range strings.Split(string(content), "\n") { + for _, line := range strings.Split(string(content), config.NewlineLF) { existing[strings.TrimSpace(line)] = true } @@ -314,12 +314,12 @@ func ensureGitignoreEntries(cmd *cobra.Command) error { // Build block to append. var sb strings.Builder - if len(content) > 0 && !strings.HasSuffix(string(content), "\n") { - sb.WriteString("\n") + if len(content) > 0 && !strings.HasSuffix(string(content), config.NewlineLF) { + sb.WriteString(config.NewlineLF) } sb.WriteString("\n# ctx managed entries\n") for _, entry := range missing { - sb.WriteString(entry + "\n") + sb.WriteString(entry + config.NewlineLF) } if err := os.WriteFile(gitignorePath, append(content, []byte(sb.String())...), config.PermFile); err != nil { @@ -327,7 +327,7 @@ func ensureGitignoreEntries(cmd *cobra.Command) error { } green := color.New(color.FgGreen).SprintFunc() - cmd.Printf(" %s .gitignore updated (%d entries added)\n", green("✓"), len(missing)) + cmd.Println(fmt.Sprintf(" %s .gitignore updated (%d entries added)", green("✓"), len(missing))) cmd.Println(" Review with: cat .gitignore") return nil } @@ -351,7 +351,7 @@ func addToGitignore(contextDir, filename string) error { } // Check if already present - lines := strings.Split(string(content), "\n") + lines := strings.Split(string(content), config.NewlineLF) for _, line := range lines { if strings.TrimSpace(line) == entry { return nil // already present @@ -360,10 +360,10 @@ func addToGitignore(contextDir, filename string) error { // Append entry var newContent string - if len(content) > 0 && !strings.HasSuffix(string(content), "\n") { - newContent = string(content) + "\n" + entry + "\n" + if len(content) > 0 && !strings.HasSuffix(string(content), config.NewlineLF) { + newContent = string(content) + config.NewlineLF + entry + config.NewlineLF } else { - newContent = string(content) + entry + "\n" + newContent = string(content) + entry + config.NewlineLF } return os.WriteFile(gitignorePath, []byte(newContent), config.PermFile) diff --git a/internal/cli/initialize/tpl.go b/internal/cli/initialize/tpl.go index 55294c11..aa863ec3 100644 --- a/internal/cli/initialize/tpl.go +++ b/internal/cli/initialize/tpl.go @@ -53,7 +53,7 @@ func createEntryTemplates( // Check if the file exists and --force not set if _, err := os.Stat(targetPath); err == nil && !force { - cmd.Printf(" %s templates/%s (exists, skipped)\n", yellow("○"), name) + cmd.Println(fmt.Sprintf(" %s templates/%s (exists, skipped)", yellow("○"), name)) continue } @@ -66,7 +66,7 @@ func createEntryTemplates( return fmt.Errorf("failed to write %s: %w", targetPath, err) } - cmd.Printf(" %s templates/%s\n", green("✓"), name) + cmd.Println(fmt.Sprintf(" %s templates/%s", green("✓"), name)) } return nil diff --git a/internal/cli/initialize/validate.go b/internal/cli/initialize/validate.go index 8a15e87e..380eb5cc 100644 --- a/internal/cli/initialize/validate.go +++ b/internal/cli/initialize/validate.go @@ -37,13 +37,13 @@ func checkCtxInPath(cmd *cobra.Command) error { red := color.New(color.FgRed).SprintFunc() yellow := color.New(color.FgYellow).SprintFunc() - cmd.PrintErrf("%s ctx is not in your PATH\n\n", red("Error:")) + cmd.PrintErrln(fmt.Sprintf("%s ctx is not in your PATH", red("Error:"))) cmd.PrintErrln( "The hooks created by 'ctx init' require ctx to be in your PATH.", ) cmd.PrintErrln("Without this, Claude Code hooks will fail silently.") cmd.PrintErrln() - cmd.PrintErrf("%s\n", yellow("To fix this:")) + cmd.PrintErrln(yellow("To fix this:")) cmd.PrintErrln(" 1. Build: make build") cmd.PrintErrln(" 2. Install: sudo make install") cmd.PrintErrln() diff --git a/internal/cli/load/convert.go b/internal/cli/load/convert.go index 765f9434..bd001761 100644 --- a/internal/cli/load/convert.go +++ b/internal/cli/load/convert.go @@ -6,7 +6,10 @@ package load -import "strings" +import ( + "github.com/ActiveMemory/ctx/internal/config" + "strings" +) // fileNameToTitle converts a context file name to a human-readable title. // @@ -21,7 +24,7 @@ import "strings" // - string: Title case representation of the file name func fileNameToTitle(name string) string { // Remove .md extension - name = strings.TrimSuffix(name, ".md") + name = strings.TrimSuffix(name, config.ExtMarkdown) // Convert SCREAMING_SNAKE to Title Case name = strings.ReplaceAll(name, "_", " ") // Title case each word diff --git a/internal/cli/notify/doc.go b/internal/cli/notify/doc.go new file mode 100644 index 00000000..78a78c4d --- /dev/null +++ b/internal/cli/notify/doc.go @@ -0,0 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package notify implements the ctx notify command for sending webhook +// notifications. +package notify diff --git a/internal/cli/pad/add.go b/internal/cli/pad/add.go index 4423c356..67de1927 100644 --- a/internal/cli/pad/add.go +++ b/internal/cli/pad/add.go @@ -50,7 +50,7 @@ func runAdd(cmd *cobra.Command, text string) error { return err } - cmd.Printf("Added entry %d.\n", len(entries)) + cmd.Println(fmt.Sprintf("Added entry %d.", len(entries))) return nil } @@ -76,6 +76,6 @@ func runAddBlob(cmd *cobra.Command, label, filePath string) error { return err } - cmd.Printf("Added entry %d.\n", len(entries)) + cmd.Println(fmt.Sprintf("Added entry %d.", len(entries))) return nil } diff --git a/internal/cli/pad/edit.go b/internal/cli/pad/edit.go index 07336088..10e02807 100644 --- a/internal/cli/pad/edit.go +++ b/internal/cli/pad/edit.go @@ -132,7 +132,7 @@ func runEdit(cmd *cobra.Command, n int, text string) error { return err } - cmd.Printf("Updated entry %d.\n", n) + cmd.Println(fmt.Sprintf("Updated entry %d.", n)) return nil } @@ -158,7 +158,7 @@ func runEditAppend(cmd *cobra.Command, n int, text string) error { return err } - cmd.Printf("Updated entry %d.\n", n) + cmd.Println(fmt.Sprintf("Updated entry %d.", n)) return nil } @@ -184,7 +184,7 @@ func runEditPrepend(cmd *cobra.Command, n int, text string) error { return err } - cmd.Printf("Updated entry %d.\n", n) + cmd.Println(fmt.Sprintf("Updated entry %d.", n)) return nil } @@ -228,6 +228,6 @@ func runEditBlob(cmd *cobra.Command, n int, filePath, labelText string) error { return err } - cmd.Printf("Updated entry %d.\n", n) + cmd.Println(fmt.Sprintf("Updated entry %d.", n)) return nil } diff --git a/internal/cli/pad/export.go b/internal/cli/pad/export.go index 1f0411fc..84172058 100644 --- a/internal/cli/pad/export.go +++ b/internal/cli/pad/export.go @@ -76,27 +76,27 @@ func runExport(cmd *cobra.Command, dir string, force, dryRun bool) error { ts := fmt.Sprintf("%d", time.Now().Unix()) newName := ts + "-" + label if dryRun { - cmd.Printf(" %s → %s (exists)\n", label, filepath.Join(dir, newName)) + cmd.Println(fmt.Sprintf(" %s → %s (exists)", label, filepath.Join(dir, newName))) count++ continue } outPath = filepath.Join(dir, newName) - cmd.Printf(" ! %s exists, writing as %s\n", label, newName) + cmd.Println(fmt.Sprintf(" ! %s exists, writing as %s", label, newName)) } } if dryRun { - cmd.Printf(" %s → %s\n", label, outPath) + cmd.Println(fmt.Sprintf(" %s → %s", label, outPath)) count++ continue } if err := os.WriteFile(outPath, data, 0o600); err != nil { - cmd.PrintErrf(" ! failed to write %s: %v\n", label, err) + cmd.PrintErrln(fmt.Sprintf(" ! failed to write %s: %v", label, err)) continue } - cmd.Printf(" + %s\n", label) + cmd.Println(fmt.Sprintf(" + %s", label)) count++ } @@ -109,6 +109,6 @@ func runExport(cmd *cobra.Command, dir string, force, dryRun bool) error { if dryRun { verb = "Would export" } - cmd.Printf("%s %d blobs.\n", verb, count) + cmd.Println(fmt.Sprintf("%s %d blobs.", verb, count)) return nil } diff --git a/internal/cli/pad/import.go b/internal/cli/pad/import.go index 06f7beff..267deee4 100644 --- a/internal/cli/pad/import.go +++ b/internal/cli/pad/import.go @@ -96,7 +96,7 @@ func runImport(cmd *cobra.Command, file string) error { return err } - cmd.Printf("Imported %d entries.\n", count) + cmd.Println(fmt.Sprintf("Imported %d entries.", count)) return nil } @@ -133,20 +133,20 @@ func runImportBlobs(cmd *cobra.Command, path string) error { data, fileErr := os.ReadFile(filePath) //nolint:gosec // user-provided path is intentional if fileErr != nil { - cmd.PrintErrf(" ! skipped: %s (%v)\n", name, fileErr) + cmd.PrintErrln(fmt.Sprintf(" ! skipped: %s (%v)", name, fileErr)) skipped++ continue } if len(data) > MaxBlobSize { - cmd.PrintErrf(" ! skipped: %s (exceeds %d byte limit)\n", - name, MaxBlobSize) + cmd.PrintErrln(fmt.Sprintf(" ! skipped: %s (exceeds %d byte limit)", + name, MaxBlobSize)) skipped++ continue } entries = append(entries, makeBlob(name, data)) - cmd.Printf(" + %s\n", name) + cmd.Println(fmt.Sprintf(" + %s", name)) added++ } @@ -161,6 +161,6 @@ func runImportBlobs(cmd *cobra.Command, path string) error { } } - cmd.Printf("Done. Added %d, skipped %d.\n", added, skipped) + cmd.Println(fmt.Sprintf("Done. Added %d, skipped %d.", added, skipped)) return nil } diff --git a/internal/cli/pad/merge.go b/internal/cli/pad/merge.go index b6f2e7d6..82e1d156 100644 --- a/internal/cli/pad/merge.go +++ b/internal/cli/pad/merge.go @@ -100,21 +100,21 @@ func runMerge( for _, entry := range entries { if seen[entry] { dupes++ - cmd.Printf( + cmd.Println(fmt.Sprintf( " = %-40s (duplicate, skipped)\n", displayEntry(entry), - ) + )) continue } seen[entry] = true checkBlobConflict(cmd, entry, blobLabels) newEntries = append(newEntries, entry) added++ - cmd.Printf( + cmd.Println(fmt.Sprintf( " + %-40s (from %s)\n", displayEntry(entry), file, - ) + )) } } @@ -124,22 +124,22 @@ func runMerge( } if added == 0 { - cmd.Printf( + cmd.Println(fmt.Sprintf( "No new entries to merge (%d %s skipped).\n", dupes, pluralize("duplicate", dupes), - ) + )) return nil } if dryRun { - cmd.Printf( + cmd.Println(fmt.Sprintf( "Would merge %d new %s (%d %s skipped).\n", added, pluralize("entry", added), dupes, pluralize("duplicate", dupes), - ) + )) return nil } @@ -150,13 +150,13 @@ func runMerge( return writeErr } - cmd.Printf( + cmd.Println(fmt.Sprintf( "Merged %d new %s (%d %s skipped).\n", added, pluralize("entry", added), dupes, pluralize("duplicate", dupes), - ) + )) return nil } @@ -254,10 +254,10 @@ func checkBlobConflict( existing, found := blobLabels[label] if found && existing != entry { - cmd.Printf( + cmd.Println(fmt.Sprintf( " ! blob %q has different content across sources; both kept\n", label, - ) + )) } blobLabels[label] = entry @@ -273,11 +273,11 @@ func checkBlobConflict( func warnIfBinary(cmd *cobra.Command, file string, entries []string) { for _, entry := range entries { if !utf8.ValidString(entry) { - cmd.Printf( + cmd.Println(fmt.Sprintf( " ! %s appears to contain binary data;"+ " it may be encrypted (use --key)\n", file, - ) + )) return } } diff --git a/internal/cli/pad/mv.go b/internal/cli/pad/mv.go index 49e054c4..ae319a17 100644 --- a/internal/cli/pad/mv.go +++ b/internal/cli/pad/mv.go @@ -7,6 +7,7 @@ package pad import ( + "fmt" "strconv" "github.com/spf13/cobra" @@ -61,6 +62,6 @@ func runMv(cmd *cobra.Command, n, m int) error { return err } - cmd.Printf("Moved entry %d to %d.\n", n, m) + cmd.Println(fmt.Sprintf("Moved entry %d to %d.", n, m)) return nil } diff --git a/internal/cli/pad/pad.go b/internal/cli/pad/pad.go index 8f221002..99d7063c 100644 --- a/internal/cli/pad/pad.go +++ b/internal/cli/pad/pad.go @@ -76,7 +76,7 @@ func runList(cmd *cobra.Command) error { } for i, entry := range entries { - cmd.Printf(" %d. %s\n", i+1, displayEntry(entry)) + cmd.Println(fmt.Sprintf(" %d. %s", i+1, displayEntry(entry))) } return nil diff --git a/internal/cli/pad/resolve.go b/internal/cli/pad/resolve.go index 4ade9cc5..d154a062 100644 --- a/internal/cli/pad/resolve.go +++ b/internal/cli/pad/resolve.go @@ -68,14 +68,14 @@ func runResolve(cmd *cobra.Command) error { if errOurs == nil { cmd.Println("=== OURS ===") for i, entry := range ours { - cmd.Printf(" %d. %s\n", i+1, displayEntry(entry)) + cmd.Println(fmt.Sprintf(" %d. %s", i+1, displayEntry(entry))) } } if errTheirs == nil { cmd.Println("=== THEIRS ===") for i, entry := range theirs { - cmd.Printf(" %d. %s\n", i+1, displayEntry(entry)) + cmd.Println(fmt.Sprintf(" %d. %s", i+1, displayEntry(entry))) } } diff --git a/internal/cli/pad/rm.go b/internal/cli/pad/rm.go index a9a4328f..759706dd 100644 --- a/internal/cli/pad/rm.go +++ b/internal/cli/pad/rm.go @@ -7,6 +7,7 @@ package pad import ( + "fmt" "strconv" "github.com/spf13/cobra" @@ -48,6 +49,6 @@ func runRm(cmd *cobra.Command, n int) error { return err } - cmd.Printf("Removed entry %d.\n", n) + cmd.Println(fmt.Sprintf("Removed entry %d.", n)) return nil } diff --git a/internal/cli/pad/show.go b/internal/cli/pad/show.go index 7d66e5b3..55d80ba8 100644 --- a/internal/cli/pad/show.go +++ b/internal/cli/pad/show.go @@ -79,7 +79,7 @@ func runShow(cmd *cobra.Command, n int, outPath string) error { if err := os.WriteFile(outPath, data, 0600); err != nil { return fmt.Errorf("write file: %w", err) } - cmd.Printf("Wrote %d bytes to %s\n", len(data), outPath) + cmd.Println(fmt.Sprintf("Wrote %d bytes to %s", len(data), outPath)) return nil } cmd.Print(string(data)) diff --git a/internal/cli/pad/store.go b/internal/cli/pad/store.go index 52445d13..f89fd804 100644 --- a/internal/cli/pad/store.go +++ b/internal/cli/pad/store.go @@ -98,17 +98,17 @@ func ensureGitignore(contextDir, filename string) error { return err } - for _, line := range strings.Split(string(content), "\n") { + for _, line := range strings.Split(string(content), config.NewlineLF) { if strings.TrimSpace(line) == entry { return nil } } sep := "" - if len(content) > 0 && !strings.HasSuffix(string(content), "\n") { - sep = "\n" + if len(content) > 0 && !strings.HasSuffix(string(content), config.NewlineLF) { + sep = config.NewlineLF } - return os.WriteFile(gitignorePath, []byte(string(content)+sep+entry+"\n"), config.PermFile) + return os.WriteFile(gitignorePath, []byte(string(content)+sep+entry+config.NewlineLF), config.PermFile) } // readEntries reads the scratchpad and returns its entries. @@ -197,7 +197,7 @@ func parseEntries(data []byte) []string { if len(data) == 0 { return nil } - lines := strings.Split(string(data), "\n") + lines := strings.Split(string(data), config.NewlineLF) var entries []string for _, line := range lines { if line != "" { @@ -212,5 +212,5 @@ func formatEntries(entries []string) []byte { if len(entries) == 0 { return nil } - return []byte(strings.Join(entries, "\n") + "\n") + return []byte(strings.Join(entries, config.NewlineLF) + config.NewlineLF) } diff --git a/internal/cli/pause/doc.go b/internal/cli/pause/doc.go new file mode 100644 index 00000000..f3e129b8 --- /dev/null +++ b/internal/cli/pause/doc.go @@ -0,0 +1,8 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package pause pauses all context hooks for the current session. +package pause diff --git a/internal/cli/pause/pause.go b/internal/cli/pause/pause.go index bcae764e..d425b65b 100644 --- a/internal/cli/pause/pause.go +++ b/internal/cli/pause/pause.go @@ -12,6 +12,7 @@ package pause import ( + "fmt" "os" "github.com/spf13/cobra" @@ -35,7 +36,7 @@ Resume with: ctx resume`, sessionID = system.ReadSessionID(os.Stdin) } system.Pause(sessionID) - cmd.Printf("Context hooks paused for session %s\n", sessionID) + cmd.Println(fmt.Sprintf("Context hooks paused for session %s", sessionID)) return nil }, } diff --git a/internal/cli/permissions/run.go b/internal/cli/permissions/run.go index d11a3983..9fb0cecd 100644 --- a/internal/cli/permissions/run.go +++ b/internal/cli/permissions/run.go @@ -9,6 +9,7 @@ package permissions import ( "bytes" "encoding/json" + "fmt" "os" "github.com/spf13/cobra" @@ -37,7 +38,7 @@ func runSnapshot(cmd *cobra.Command) error { return errWriteFile(config.FileSettingsGolden, err) } - cmd.Printf("%s golden image: %s\n", verb, config.FileSettingsGolden) + cmd.Println(fmt.Sprintf("%s golden image: %s", verb, config.FileSettingsGolden)) return nil } @@ -83,27 +84,27 @@ func runRestore(cmd *cobra.Command) error { denyRestored, denyDropped := diffStringSlices(golden.Permissions.Deny, local.Permissions.Deny) if len(dropped) > 0 { - cmd.Printf("Dropped %d session allow permission(s):\n", len(dropped)) + cmd.Println(fmt.Sprintf("Dropped %d session allow permission(s):", len(dropped))) for _, p := range dropped { - cmd.Printf(" - %s\n", p) + cmd.Println(fmt.Sprintf(" - %s", p)) } } if len(restored) > 0 { - cmd.Printf("Restored %d allow permission(s):\n", len(restored)) + cmd.Println(fmt.Sprintf("Restored %d allow permission(s):", len(restored))) for _, p := range restored { - cmd.Printf(" + %s\n", p) + cmd.Println(fmt.Sprintf(" + %s", p)) } } if len(denyDropped) > 0 { - cmd.Printf("Dropped %d session deny rule(s):\n", len(denyDropped)) + cmd.Println(fmt.Sprintf("Dropped %d session deny rule(s):", len(denyDropped))) for _, p := range denyDropped { - cmd.Printf(" - %s\n", p) + cmd.Println(fmt.Sprintf(" - %s", p)) } } if len(denyRestored) > 0 { - cmd.Printf("Restored %d deny rule(s):\n", len(denyRestored)) + cmd.Println(fmt.Sprintf("Restored %d deny rule(s):", len(denyRestored))) for _, p := range denyRestored { - cmd.Printf(" + %s\n", p) + cmd.Println(fmt.Sprintf(" + %s", p)) } } allEmpty := len(dropped) == 0 && len(restored) == 0 && len(denyDropped) == 0 && len(denyRestored) == 0 diff --git a/internal/cli/prompt/add.go b/internal/cli/prompt/add.go index 6a508add..29e1f6f1 100644 --- a/internal/cli/prompt/add.go +++ b/internal/cli/prompt/add.go @@ -54,7 +54,7 @@ func runAdd(cmd *cobra.Command, name string, fromStdin bool) error { return fmt.Errorf("create prompts directory: %w", err) } - path := filepath.Join(dir, name+".md") + path := filepath.Join(dir, name+config.ExtMarkdown) // Check if file already exists. if _, err := os.Stat(path); err == nil { @@ -72,7 +72,7 @@ func runAdd(cmd *cobra.Command, name string, fromStdin bool) error { } else { // Try to load from embedded starter templates. var err error - content, err = assets.PromptTemplate(name + ".md") + content, err = assets.PromptTemplate(name + config.ExtMarkdown) if err != nil { return fmt.Errorf("no embedded template %q — use --stdin to provide content", name) } @@ -82,6 +82,6 @@ func runAdd(cmd *cobra.Command, name string, fromStdin bool) error { return fmt.Errorf("write prompt: %w", err) } - cmd.Printf("Created prompt %q.\n", name) + cmd.Println(fmt.Sprintf("Created prompt %q.", name)) return nil } diff --git a/internal/cli/prompt/prompt.go b/internal/cli/prompt/prompt.go index a121380b..203ff0ae 100644 --- a/internal/cli/prompt/prompt.go +++ b/internal/cli/prompt/prompt.go @@ -87,10 +87,10 @@ func runList(cmd *cobra.Command) error { var found bool for _, entry := range entries { name := entry.Name() - if entry.IsDir() || !strings.HasSuffix(name, ".md") { + if entry.IsDir() || !strings.HasSuffix(name, config.ExtMarkdown) { continue } - cmd.Printf(" %s\n", strings.TrimSuffix(name, ".md")) + cmd.Println(fmt.Sprintf(" %s", strings.TrimSuffix(name, config.ExtMarkdown))) found = true } diff --git a/internal/cli/prompt/rm.go b/internal/cli/prompt/rm.go index 784b458b..180bcded 100644 --- a/internal/cli/prompt/rm.go +++ b/internal/cli/prompt/rm.go @@ -8,6 +8,7 @@ package prompt import ( "fmt" + "github.com/ActiveMemory/ctx/internal/config" "os" "path/filepath" @@ -31,7 +32,7 @@ func rmCmd() *cobra.Command { // runRm deletes a prompt template by name. func runRm(cmd *cobra.Command, name string) error { - path := filepath.Join(promptsDir(), name+".md") + path := filepath.Join(promptsDir(), name+config.ExtMarkdown) if _, err := os.Stat(path); os.IsNotExist(err) { return fmt.Errorf("prompt %q not found", name) @@ -41,6 +42,6 @@ func runRm(cmd *cobra.Command, name string) error { return fmt.Errorf("remove prompt: %w", err) } - cmd.Printf("Removed prompt %q.\n", name) + cmd.Println(fmt.Sprintf("Removed prompt %q.", name)) return nil } diff --git a/internal/cli/prompt/show.go b/internal/cli/prompt/show.go index d60972c9..c10998aa 100644 --- a/internal/cli/prompt/show.go +++ b/internal/cli/prompt/show.go @@ -8,6 +8,7 @@ package prompt import ( "fmt" + "github.com/ActiveMemory/ctx/internal/config" "os" "path/filepath" @@ -31,7 +32,7 @@ func showCmd() *cobra.Command { // runShow reads and prints a prompt template by name. func runShow(cmd *cobra.Command, name string) error { - path := filepath.Join(promptsDir(), name+".md") + path := filepath.Join(promptsDir(), name+config.ExtMarkdown) content, err := os.ReadFile(path) //nolint:gosec // user-provided name is intentional if err != nil { diff --git a/internal/cli/recall/index.go b/internal/cli/recall/index.go index 03845b25..0786cf1e 100644 --- a/internal/cli/recall/index.go +++ b/internal/cli/recall/index.go @@ -21,7 +21,7 @@ import ( // Two-pass matching: // 1. Parse YAML frontmatter for a session_id field (authoritative). // 2. For files without session_id, extract the last 8 characters before -// ".md" and treat them as a short ID candidate (migration path for +// config.ExtMarkdown and treat them as a short ID candidate (migration path for // legacy exports). // // Parameters: @@ -62,7 +62,7 @@ func buildSessionIndex(journalDir string) map[string]string { // Pass 2: extract short ID from filename as fallback. // Filename format: YYYY-MM-DD-slug-SHORTID.md or ...-pN.md name := e.Name() - // Strip multipart suffix (e.g., "-p2.md" → ".md"). + // Strip multipart suffix (e.g., "-p2.md" → config.ExtMarkdown). baseName := strings.TrimSuffix(name, config.ExtMarkdown) if idx := strings.LastIndex(baseName, "-p"); idx > 0 { suffix := baseName[idx+2:] diff --git a/internal/cli/recall/lock.go b/internal/cli/recall/lock.go index 50d1bcbf..49ea5b16 100644 --- a/internal/cli/recall/lock.go +++ b/internal/cli/recall/lock.go @@ -168,7 +168,7 @@ func runLockUnlock( path := filepath.Join(journalDir, filename) updateLockFrontmatter(path, lock) - cmd.Printf(" %s %s (%s)\n", green("✓"), filename, verb) + cmd.Println(fmt.Sprintf(" %s %s (%s)", green("✓"), filename, verb)) count++ } @@ -177,9 +177,9 @@ func runLockUnlock( } if count == 0 { - cmd.Printf("No changes — all matched entries already %s.\n", verb) + cmd.Println(fmt.Sprintf("No changes — all matched entries already %s.", verb)) } else { - cmd.Printf("\n%s %d entry(s).\n", strings.Title(verb), count) //nolint:staticcheck // strings.Title is fine for single words + cmd.Println(fmt.Sprintf("\n%s %d entry(s).", strings.Title(verb), count)) //nolint:staticcheck // strings.Title is fine for single words } return nil diff --git a/internal/cli/recall/run.go b/internal/cli/recall/run.go index a0d4e308..ff112ba7 100644 --- a/internal/cli/recall/run.go +++ b/internal/cli/recall/run.go @@ -141,7 +141,7 @@ func planExport( slug, title := titleSlug(s, existingTitle) baseFilename := formatJournalFilename(s, slug) - baseName := strings.TrimSuffix(baseFilename, ".md") + baseName := strings.TrimSuffix(baseFilename, config.ExtMarkdown) // Detect renames (dedup: old slug → new slug). if oldFile := lookupSessionFile(sessionIndex, s.ID); oldFile != "" { @@ -237,7 +237,7 @@ func printExportSummary(cmd *cobra.Command, plan exportPlan, isDryRun bool) { cmd.Println("Nothing to export.") return } - cmd.Printf("%s %s.\n", verb, strings.Join(parts, ", ")) + cmd.Println(fmt.Sprintf("%s %s.", verb, strings.Join(parts, ", "))) } // confirmExport prints the plan summary and prompts for confirmation. @@ -297,7 +297,7 @@ func executeExport( existing, readErr := os.ReadFile(filepath.Clean(fa.path)) if readErr == nil { if fm := extractFrontmatter(string(existing)); fm != "" { - content = fm + "\n" + stripFrontmatter(content) + content = fm + config.NewlineLF + stripFrontmatter(content) } } } @@ -312,16 +312,16 @@ func executeExport( // Write file. if err := os.WriteFile(fa.path, []byte(content), config.PermFile); err != nil { - cmd.PrintErrf(" %s failed to write %s: %v\n", yellow("!"), fa.filename, err) + cmd.PrintErrln(fmt.Sprintf(" %s failed to write %s: %v", yellow("!"), fa.filename, err)) continue } jstate.MarkExported(fa.filename) if fileExists && !discard { - cmd.Printf(" %s %s (updated, frontmatter preserved)\n", green("✓"), fa.filename) + cmd.Println(fmt.Sprintf(" %s %s (updated, frontmatter preserved)", green("✓"), fa.filename)) } else { - cmd.Printf(" %s %s\n", green("✓"), fa.filename) + cmd.Println(fmt.Sprintf(" %s %s", green("✓"), fa.filename)) } } @@ -376,10 +376,10 @@ func runRecallExport(cmd *cobra.Command, args []string, opts exportOpts) error { return fmt.Errorf("session not found: %s", args[0]) } if len(toExport) > 1 { - cmd.PrintErrf("Multiple sessions match '%s':\n", args[0]) + cmd.PrintErrln(fmt.Sprintf("Multiple sessions match '%s':", args[0])) for _, m := range toExport { - cmd.PrintErrf(" %s (%s) - %s\n", - m.Slug, m.ID[:8], m.StartTime.Format("2006-01-02 15:04")) + cmd.PrintErrln(fmt.Sprintf(" %s (%s) - %s", + m.Slug, m.ID[:8], m.StartTime.Format("2006-01-02 15:04"))) } return fmt.Errorf("ambiguous query, use a more specific ID") } @@ -433,19 +433,19 @@ func runRecallExport(cmd *cobra.Command, args []string, opts exportOpts) error { // 11. Persist journal state. if err := jstate.Save(journalDir); err != nil { - cmd.PrintErrf("warning: failed to save journal state: %v\n", err) + cmd.PrintErrln(fmt.Sprintf("warning: failed to save journal state: %v", err)) } // 12. Print final summary. cmd.Println() if exported > 0 { - cmd.Printf("Exported %d new session(s) to %s\n", exported, journalDir) + cmd.Println(fmt.Sprintf("Exported %d new session(s) to %s", exported, journalDir)) } if updated > 0 { - cmd.Printf("Updated %d existing session(s) (YAML frontmatter preserved)\n", updated) + cmd.Println(fmt.Sprintf("Updated %d existing session(s) (YAML frontmatter preserved)", updated)) } if renamed > 0 { - cmd.Printf("Renamed %d session(s) to title-based filenames\n", renamed) + cmd.Println(fmt.Sprintf("Renamed %d session(s) to title-based filenames", renamed)) } dim := color.New(color.FgHiBlack) if skipped > 0 { diff --git a/internal/cli/recall/sync.go b/internal/cli/recall/sync.go index 800f89c8..7e2edb8a 100644 --- a/internal/cli/recall/sync.go +++ b/internal/cli/recall/sync.go @@ -94,11 +94,11 @@ func runSync(cmd *cobra.Command) error { switch { case fmLocked && !stateLocked: jstate.Mark(filename, "locked") - cmd.Printf(" %s %s (locked)\n", green("✓"), filename) + cmd.Println(fmt.Sprintf(" %s %s (locked)", green("✓"), filename)) locked++ case !fmLocked && stateLocked: jstate.Clear(filename, "locked") - cmd.Printf(" %s %s (unlocked)\n", yellow("✓"), filename) + cmd.Println(fmt.Sprintf(" %s %s (unlocked)", yellow("✓"), filename)) unlocked++ } } @@ -111,10 +111,10 @@ func runSync(cmd *cobra.Command) error { cmd.Println("No changes — state already matches frontmatter.") } else { if locked > 0 { - cmd.Printf("\nLocked %d entry(s).\n", locked) + cmd.Println(fmt.Sprintf("\nLocked %d entry(s).", locked)) } if unlocked > 0 { - cmd.Printf("\nUnlocked %d entry(s).\n", unlocked) + cmd.Println(fmt.Sprintf("\nUnlocked %d entry(s).", unlocked)) } } diff --git a/internal/cli/remind/doc.go b/internal/cli/remind/doc.go new file mode 100644 index 00000000..d3501dbc --- /dev/null +++ b/internal/cli/remind/doc.go @@ -0,0 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package remind manages session-scoped reminders stored in +// .context/reminders.json. +package remind diff --git a/internal/cli/remind/remind.go b/internal/cli/remind/remind.go index 7488f781..52151bfe 100644 --- a/internal/cli/remind/remind.go +++ b/internal/cli/remind/remind.go @@ -132,7 +132,7 @@ func runAdd(cmd *cobra.Command, message, after string) error { if r.After != nil { suffix = fmt.Sprintf(" (after %s)", *r.After) } - cmd.Printf(" + [%d] %s%s\n", r.ID, r.Message, suffix) + cmd.Println(fmt.Sprintf(" + [%d] %s%s", r.ID, r.Message, suffix)) return nil } @@ -155,7 +155,7 @@ func runList(cmd *cobra.Command) error { annotation = fmt.Sprintf(" (after %s, not yet due)", *r.After) } } - cmd.Printf(" [%d] %s%s\n", r.ID, r.Message, annotation) + cmd.Println(fmt.Sprintf(" [%d] %s%s", r.ID, r.Message, annotation)) } return nil @@ -184,7 +184,7 @@ func runDismiss(cmd *cobra.Command, idStr string) error { return fmt.Errorf("no reminder with ID %d", id) } - cmd.Printf(" - [%d] %s\n", reminders[found].ID, reminders[found].Message) + cmd.Println(fmt.Sprintf(" - [%d] %s", reminders[found].ID, reminders[found].Message)) reminders = append(reminders[:found], reminders[found+1:]...) return WriteReminders(reminders) } @@ -201,9 +201,9 @@ func runDismissAll(cmd *cobra.Command) error { } for _, r := range reminders { - cmd.Printf(" - [%d] %s\n", r.ID, r.Message) + cmd.Println(fmt.Sprintf(" - [%d] %s", r.ID, r.Message)) } - cmd.Printf("Dismissed %d reminders.\n", len(reminders)) + cmd.Println(fmt.Sprintf("Dismissed %d reminders.", len(reminders))) return WriteReminders([]Reminder{}) } diff --git a/internal/cli/resume/doc.go b/internal/cli/resume/doc.go new file mode 100644 index 00000000..80a499a1 --- /dev/null +++ b/internal/cli/resume/doc.go @@ -0,0 +1,8 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package resume resumes context hooks after a pause. +package resume diff --git a/internal/cli/resume/resume.go b/internal/cli/resume/resume.go index ffd24758..89f59e9f 100644 --- a/internal/cli/resume/resume.go +++ b/internal/cli/resume/resume.go @@ -11,6 +11,7 @@ package resume import ( + "fmt" "os" "github.com/spf13/cobra" @@ -32,7 +33,7 @@ The session ID is read from stdin JSON (same as hooks) or --session-id flag.`, sessionID = system.ReadSessionID(os.Stdin) } system.Resume(sessionID) - cmd.Printf("Context hooks resumed for session %s\n", sessionID) + cmd.Println(fmt.Sprintf("Context hooks resumed for session %s", sessionID)) return nil }, } diff --git a/internal/cli/site/feed.go b/internal/cli/site/feed.go index ddde2125..8f9d14c0 100644 --- a/internal/cli/site/feed.go +++ b/internal/cli/site/feed.go @@ -350,9 +350,9 @@ func generateAtom( entryURL := blogURL + slug + "/" entry := AtomEntry{ - Title: p.title, - Links: []AtomLink{{Href: entryURL}}, - ID: entryURL, + Title: p.title, + Links: []AtomLink{{Href: entryURL}}, + ID: entryURL, Updated: p.date + "T00:00:00Z", } diff --git a/internal/cli/system/backup.go b/internal/cli/system/backup.go index 5322fd77..000dbc8a 100644 --- a/internal/cli/system/backup.go +++ b/internal/cli/system/backup.go @@ -126,10 +126,10 @@ func runBackup(cmd *cobra.Command) error { } for _, r := range results { - cmd.Printf("%s: %s (%s)", - r.Scope, r.Archive, formatSize(r.Size)) + cmd.Print(fmt.Sprintf("%s: %s (%s)", + r.Scope, r.Archive, formatSize(r.Size))) if r.SMBDest != "" { - cmd.Printf(" → %s", r.SMBDest) + cmd.Print(fmt.Sprintf(" → %s", r.SMBDest)) } cmd.Println() } @@ -248,7 +248,7 @@ func addEntry(tw *tar.Writer, entry archiveEntry, cmd *cobra.Command) error { info, statErr := os.Stat(entry.SourcePath) if os.IsNotExist(statErr) { if entry.Optional { - cmd.PrintErrf("skipping %s (not found)\n", entry.Prefix) + cmd.PrintErrln(fmt.Sprintf("skipping %s (not found)", entry.Prefix)) return nil } return fmt.Errorf("source not found: %s", entry.SourcePath) diff --git a/internal/cli/system/bootstrap.go b/internal/cli/system/bootstrap.go index 5caa946d..48c41a97 100644 --- a/internal/cli/system/bootstrap.go +++ b/internal/cli/system/bootstrap.go @@ -9,6 +9,7 @@ package system import ( "encoding/json" "fmt" + "github.com/ActiveMemory/ctx/internal/config" "os" "path/filepath" "sort" @@ -151,7 +152,7 @@ func listContextFiles(dir string) []string { if e.IsDir() { continue } - if strings.EqualFold(filepath.Ext(e.Name()), ".md") { + if strings.EqualFold(filepath.Ext(e.Name()), config.ExtMarkdown) { files = append(files, e.Name()) } } @@ -189,5 +190,5 @@ func wrapFileList(files []string, maxWidth int, indent string) string { } } lines = append(lines, current) - return strings.Join(lines, "\n") + return strings.Join(lines, config.NewlineLF) } diff --git a/internal/cli/system/check_backup_age.go b/internal/cli/system/check_backup_age.go index 6eca60a1..e2190703 100644 --- a/internal/cli/system/check_backup_age.go +++ b/internal/cli/system/check_backup_age.go @@ -91,7 +91,7 @@ func runCheckBackupAge(cmd *cobra.Command, stdin *os.File) error { // Build pre-formatted warnings for the template variable var warningText string for _, w := range warnings { - warningText += w + "\n" + warningText += w + config.NewlineLF } content := loadMessage("check-backup-age", "warning", @@ -105,7 +105,7 @@ func runCheckBackupAge(cmd *cobra.Command, stdin *os.File) error { "┌─ Backup Warning ──────────────────────────────────\n" msg += boxLines(content) if line := contextDirLine(); line != "" { - msg += "│ " + line + "\n" + msg += "│ " + line + config.NewlineLF } msg += config.NudgeBoxBottom cmd.Println(msg) diff --git a/internal/cli/system/check_ceremonies.go b/internal/cli/system/check_ceremonies.go index d86725b0..fc604360 100644 --- a/internal/cli/system/check_ceremonies.go +++ b/internal/cli/system/check_ceremonies.go @@ -7,6 +7,7 @@ package system import ( + "github.com/ActiveMemory/ctx/internal/config" "os" "path/filepath" "sort" @@ -109,7 +110,7 @@ func recentJournalFiles(dir string, n int) []string { var names []string for _, e := range entries { - if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") { + if e.IsDir() || !strings.HasSuffix(e.Name(), config.ExtMarkdown) { continue } names = append(names, e.Name()) @@ -160,28 +161,28 @@ func emitCeremonyNudge(cmd *cobra.Command, remember, wrapup bool) string { boxTitle = "Session Ceremonies" fallback = "Your last 3 sessions didn't use /ctx-remember or\n" + "/ctx-wrap-up.\n" + - "\n" + + config.NewlineLF + "Start sessions with: /ctx-remember\n" + " → Loads context, shows active tasks, picks up\n" + " where you left off. No re-explaining needed.\n" + - "\n" + + config.NewlineLF + "End sessions with: /ctx-wrap-up\n" + " → Captures learnings and decisions so the next\n" + " session starts informed, not from scratch.\n" + - "\n" + + config.NewlineLF + "These take seconds and save minutes." case !remember: variant = "remember" boxTitle = "Session Start" fallback = "Try starting this session with /ctx-remember\n" + - "\n" + + config.NewlineLF + "It loads your context, shows active tasks, and\n" + "picks up where you left off — no re-explaining." case !wrapup: variant = "wrapup" boxTitle = "Session End" fallback = "Your last 3 sessions didn't end with /ctx-wrap-up\n" + - "\n" + + config.NewlineLF + "It captures learnings and decisions so the next\n" + "session starts informed, not from scratch." } @@ -192,10 +193,10 @@ func emitCeremonyNudge(cmd *cobra.Command, remember, wrapup bool) string { } msg := "IMPORTANT: Relay this session tip to the user VERBATIM before answering their question.\n\n" + - "┌─ " + boxTitle + " " + strings.Repeat("─", 51-len(boxTitle)) + "\n" + "┌─ " + boxTitle + " " + strings.Repeat("─", 51-len(boxTitle)) + config.NewlineLF msg += boxLines(content) if dirFooter := contextDirLine(); dirFooter != "" { - msg += "│ " + dirFooter + "\n" + msg += "│ " + dirFooter + config.NewlineLF } msg += "└───────────────────────────────────────────────────" diff --git a/internal/cli/system/check_context_size.go b/internal/cli/system/check_context_size.go index 7910f0f2..f1d97fed 100644 --- a/internal/cli/system/check_context_size.go +++ b/internal/cli/system/check_context_size.go @@ -149,10 +149,10 @@ func emitCheckpoint(cmd *cobra.Command, logFile, sessionID string, count, tokens "┌─ Context Checkpoint (prompt #%d) ────────────────\n", count) msg += boxLines(content) if tokens > 0 { - msg += "│ " + tokenUsageLine(tokens, pct, windowSize) + "\n" + msg += "│ " + tokenUsageLine(tokens, pct, windowSize) + config.NewlineLF } if line := contextDirLine(); line != "" { - msg += "│ " + line + "\n" + msg += "│ " + line + config.NewlineLF } msg += appendOversizeNudge() msg += boxBottom @@ -181,7 +181,7 @@ func emitWindowWarning(cmd *cobra.Command, logFile, sessionID string, count, tok "┌─ Context Window Warning ─────────────────────────\n" msg += boxLines(content) if line := contextDirLine(); line != "" { - msg += "│ " + line + "\n" + msg += "│ " + line + config.NewlineLF } msg += boxBottom cmd.Println(msg) @@ -261,7 +261,7 @@ func emitBillingWarning(cmd *cobra.Command, logFile, sessionID string, count, to "┌─ Billing Threshold ──────────────────────────────\n" msg += boxLines(content) if line := contextDirLine(); line != "" { - msg += "│ " + line + "\n" + msg += "│ " + line + config.NewlineLF } msg += boxBottom cmd.Println(msg) diff --git a/internal/cli/system/check_journal.go b/internal/cli/system/check_journal.go index 2260bc54..c0d03cb2 100644 --- a/internal/cli/system/check_journal.go +++ b/internal/cli/system/check_journal.go @@ -8,6 +8,7 @@ package system import ( "fmt" + "github.com/ActiveMemory/ctx/internal/config" "os" "path/filepath" "strings" @@ -73,7 +74,7 @@ func runCheckJournal(cmd *cobra.Command, stdin *os.File) error { } // Stage 1: Unexported sessions - newestJournal := newestMtime(jDir, ".md") + newestJournal := newestMtime(jDir, config.ExtMarkdown) unexported := countNewerFiles(claudeProjectsDir, ".jsonl", newestJournal) // Stage 2: Unenriched entries @@ -114,7 +115,7 @@ func runCheckJournal(cmd *cobra.Command, stdin *os.File) error { "┌─ Journal Reminder ─────────────────────────────\n" msg += boxLines(content) if line := contextDirLine(); line != "" { - msg += "│ " + line + "\n" + msg += "│ " + line + config.NewlineLF } msg += "└────────────────────────────────────────────────" cmd.Println(msg) diff --git a/internal/cli/system/check_knowledge.go b/internal/cli/system/check_knowledge.go index 722d998e..58c7fab2 100644 --- a/internal/cli/system/check_knowledge.go +++ b/internal/cli/system/check_knowledge.go @@ -118,7 +118,7 @@ func runCheckKnowledge(cmd *cobra.Command, stdin *os.File) error { if convThreshold > 0 { convPath := filepath.Join(contextDir, config.FileConvention) if data, err := os.ReadFile(convPath); err == nil { //nolint:gosec // project-local path - lineCount := bytes.Count(data, []byte("\n")) + lineCount := bytes.Count(data, []byte(config.NewlineLF)) if lineCount > convThreshold { findings = append(findings, finding{ file: config.FileConvention, count: lineCount, threshold: convThreshold, unit: "lines", @@ -153,7 +153,7 @@ func runCheckKnowledge(cmd *cobra.Command, stdin *os.File) error { "\u250c\u2500 Knowledge File Growth \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n" msg += boxLines(content) if line := contextDirLine(); line != "" { - msg += "\u2502 " + line + "\n" + msg += "\u2502 " + line + config.NewlineLF } msg += "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" cmd.Println(msg) diff --git a/internal/cli/system/check_map_staleness.go b/internal/cli/system/check_map_staleness.go index 2126fd54..1e55032e 100644 --- a/internal/cli/system/check_map_staleness.go +++ b/internal/cli/system/check_map_staleness.go @@ -117,7 +117,7 @@ func runCheckMapStaleness(cmd *cobra.Command, stdin *os.File) error { fallback := fmt.Sprintf("ARCHITECTURE.md hasn't been refreshed since %s\n", dateStr) + fmt.Sprintf("and there are commits touching %d modules.\n", moduleCommits) + "/ctx-map keeps architecture docs drift-free.\n" + - "\n" + + config.NewlineLF + "Want me to run /ctx-map to refresh?" content := loadMessage("check-map-staleness", "stale", map[string]any{ @@ -132,7 +132,7 @@ func runCheckMapStaleness(cmd *cobra.Command, stdin *os.File) error { "\u250c\u2500 Architecture Map Stale \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n" msg += boxLines(content) if line := contextDirLine(); line != "" { - msg += "\u2502 " + line + "\n" + msg += "\u2502 " + line + config.NewlineLF } msg += "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" cmd.Println(msg) @@ -158,5 +158,5 @@ func countModuleCommits(since string) int { if lines == "" { return 0 } - return len(strings.Split(lines, "\n")) + return len(strings.Split(lines, config.NewlineLF)) } diff --git a/internal/cli/system/check_persistence.go b/internal/cli/system/check_persistence.go index 83481d7a..3e6ed25c 100644 --- a/internal/cli/system/check_persistence.go +++ b/internal/cli/system/check_persistence.go @@ -66,7 +66,7 @@ func readPersistenceState(path string) (persistenceState, bool) { } var state persistenceState - for _, line := range strings.Split(strings.TrimSpace(string(data)), "\n") { + for _, line := range strings.Split(strings.TrimSpace(string(data)), config.NewlineLF) { parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { continue @@ -107,7 +107,7 @@ func getLatestContextMtime(contextDir string) int64 { var latest int64 for _, entry := range entries { - if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".md") { + if entry.IsDir() || !strings.HasSuffix(entry.Name(), config.ExtMarkdown) { continue } info, err := entry.Info() @@ -181,7 +181,7 @@ func runCheckPersistence(cmd *cobra.Command, stdin *os.File) error { "Have you discovered learnings, made decisions,\n" + "established conventions, or completed tasks\n" + "worth persisting?\n" + - "\n" + + config.NewlineLF + "Run /ctx-wrap-up to capture session context." content := loadMessage("check-persistence", "nudge", map[string]any{ @@ -197,7 +197,7 @@ func runCheckPersistence(cmd *cobra.Command, stdin *os.File) error { "┌─ Persistence Checkpoint (prompt #%d) ───────────\n", state.Count) msg += boxLines(content) if line := contextDirLine(); line != "" { - msg += "│ " + line + "\n" + msg += "│ " + line + config.NewlineLF } msg += config.NudgeBoxBottom cmd.Println(msg) diff --git a/internal/cli/system/check_resources.go b/internal/cli/system/check_resources.go index 2bd8ac3d..9a0e6eac 100644 --- a/internal/cli/system/check_resources.go +++ b/internal/cli/system/check_resources.go @@ -7,6 +7,7 @@ package system import ( + "github.com/ActiveMemory/ctx/internal/config" "os" "github.com/spf13/cobra" @@ -69,7 +70,7 @@ func runCheckResources(cmd *cobra.Command, stdin *os.File) error { var alertMessages string for _, a := range alerts { if a.Severity == sysinfo.SeverityDanger { - alertMessages += "\u2716 " + a.Message + "\n" + alertMessages += "\u2716 " + a.Message + config.NewlineLF } } @@ -87,7 +88,7 @@ func runCheckResources(cmd *cobra.Command, stdin *os.File) error { "\u250c\u2500 Resource Alert \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n" msg += boxLines(content) if line := contextDirLine(); line != "" { - msg += "\u2502 " + line + "\n" + msg += "\u2502 " + line + config.NewlineLF } msg += "\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500" cmd.Println(msg) diff --git a/internal/cli/system/check_version.go b/internal/cli/system/check_version.go index 1947f180..b71a3a27 100644 --- a/internal/cli/system/check_version.go +++ b/internal/cli/system/check_version.go @@ -113,7 +113,7 @@ func runCheckVersion(cmd *cobra.Command, stdin *os.File) error { "┌─ Version Mismatch ─────────────────────────────\n" msg += boxLines(content) if line := contextDirLine(); line != "" { - msg += "│ " + line + "\n" + msg += "│ " + line + config.NewlineLF } msg += "└────────────────────────────────────────────────" cmd.Println(msg) @@ -163,7 +163,7 @@ func checkKeyAge(cmd *cobra.Command, sessionID string) { "┌─ Key Rotation ──────────────────────────────────┐\n" keyMsg += boxLines(keyContent) if line := contextDirLine(); line != "" { - keyMsg += "│ " + line + "\n" + keyMsg += "│ " + line + config.NewlineLF } keyMsg += "└──────────────────────────────────────────────────┘" cmd.Println(keyMsg) diff --git a/internal/cli/system/context_load_gate.go b/internal/cli/system/context_load_gate.go index 89bfc52f..b02dbd2a 100644 --- a/internal/cli/system/context_load_gate.go +++ b/internal/cli/system/context_load_gate.go @@ -156,12 +156,12 @@ func runContextLoadGate(cmd *cobra.Command, stdin *os.File) error { ctxChanges, _ := changes.FindContextChanges(refTime) codeChanges, _ := changes.SummarizeCodeChanges(refTime) if len(ctxChanges) > 0 || codeChanges.CommitCount > 0 { - content.WriteString("\n" + changes.RenderChangesForHook( + content.WriteString(config.NewlineLF + changes.RenderChangesForHook( refLabel, ctxChanges, codeChanges)) } } - content.WriteString(strings.Repeat("=", 80) + "\n") + content.WriteString(strings.Repeat("=", 80) + config.NewlineLF) content.WriteString(fmt.Sprintf( "Context: %d files loaded (~%d tokens). "+ "Order follows config.FileReadOrder.\n\n"+ @@ -201,7 +201,7 @@ func writeOversizeFlag(contextDir string, totalTokens int, perFile []fileTokenEn var flag strings.Builder flag.WriteString("Context injection oversize warning\n") - flag.WriteString(strings.Repeat("=", 35) + "\n") + flag.WriteString(strings.Repeat("=", 35) + config.NewlineLF) flag.WriteString(fmt.Sprintf("Timestamp: %s\n", time.Now().UTC().Format(time.RFC3339))) flag.WriteString(fmt.Sprintf("Injected: %d tokens (threshold: %d)\n\n", totalTokens, threshold)) flag.WriteString("Per-file breakdown:\n") diff --git a/internal/cli/system/events.go b/internal/cli/system/events.go index 991b0d40..9d856c89 100644 --- a/internal/cli/system/events.go +++ b/internal/cli/system/events.go @@ -104,7 +104,7 @@ func outputEventsHuman(cmd *cobra.Command, events []notify.Payload) error { ts := formatEventTimestamp(e.Timestamp) hookName := extractHookName(e) msg := truncateMessage(e.Message, 60) - cmd.Printf("%-19s %-5s %-24s %s\n", ts, e.Event, hookName, msg) + cmd.Println(fmt.Sprintf("%-19s %-5s %-24s %s", ts, e.Event, hookName, msg)) } return nil } diff --git a/internal/cli/system/heartbeat.go b/internal/cli/system/heartbeat.go index 193b0a5d..4877345f 100644 --- a/internal/cli/system/heartbeat.go +++ b/internal/cli/system/heartbeat.go @@ -141,4 +141,3 @@ func readMtime(path string) int64 { func writeMtime(path string, mtime int64) { _ = os.WriteFile(path, []byte(strconv.FormatInt(mtime, 10)), 0o600) } - diff --git a/internal/cli/system/mark_journal.go b/internal/cli/system/mark_journal.go index 02cd2e0e..ba0ba417 100644 --- a/internal/cli/system/mark_journal.go +++ b/internal/cli/system/mark_journal.go @@ -83,7 +83,7 @@ func runMarkJournal(cmd *cobra.Command, filename, stage string) error { if val == "" { return fmt.Errorf("%s: %s not set", filename, stage) } - cmd.Printf("%s: %s = %s\n", filename, stage, val) + cmd.Println(fmt.Sprintf("%s: %s = %s", filename, stage, val)) return nil } @@ -95,6 +95,6 @@ func runMarkJournal(cmd *cobra.Command, filename, stage string) error { return fmt.Errorf("save journal state: %w", err) } - cmd.Printf("%s: marked %s\n", filename, stage) + cmd.Println(fmt.Sprintf("%s: marked %s", filename, stage)) return nil } diff --git a/internal/cli/system/message.go b/internal/cli/system/message.go index 7a7d15f7..eb846154 100644 --- a/internal/cli/system/message.go +++ b/internal/cli/system/message.go @@ -8,6 +8,7 @@ package system import ( "bytes" + "github.com/ActiveMemory/ctx/internal/config" "os" "path/filepath" "strings" @@ -81,10 +82,10 @@ const sessionUnknown = "unknown" // an empty trailing box line. func boxLines(content string) string { var b strings.Builder - for _, line := range strings.Split(strings.TrimRight(content, "\n"), "\n") { + for _, line := range strings.Split(strings.TrimRight(content, config.NewlineLF), config.NewlineLF) { b.WriteString("│ ") b.WriteString(line) - b.WriteString("\n") + b.WriteString(config.NewlineLF) } return b.String() } diff --git a/internal/cli/system/message_cmd.go b/internal/cli/system/message_cmd.go index 0b3da4b9..5be1360f 100644 --- a/internal/cli/system/message_cmd.go +++ b/internal/cli/system/message_cmd.go @@ -96,19 +96,19 @@ func runMessageList(cmd *cobra.Command) error { } // Table output - cmd.Printf("%-24s %-20s %-16s %s\n", "Hook", "Variant", "Category", "Override") - cmd.Printf("%-24s %-20s %-16s %s\n", + cmd.Println(fmt.Sprintf("%-24s %-20s %-16s %s", "Hook", "Variant", "Category", "Override")) + cmd.Println(fmt.Sprintf("%-24s %-20s %-16s %s", strings.Repeat("\u2500", 22), strings.Repeat("\u2500", 18), strings.Repeat("\u2500", 14), - strings.Repeat("\u2500", 8)) + strings.Repeat("\u2500", 8))) for _, e := range entries { override := "" if e.HasOverride { override = "override" } - cmd.Printf("%-24s %-20s %-16s %s\n", e.Hook, e.Variant, e.Category, override) + cmd.Println(fmt.Sprintf("%-24s %-20s %-16s %s", e.Hook, e.Variant, e.Category, override)) } return nil @@ -135,7 +135,7 @@ func runMessageShow(cmd *cobra.Command, hook, variant string) error { // Check user override first overridePath := overridePath(hook, variant) if data, readErr := os.ReadFile(overridePath); readErr == nil { //nolint:gosec // project-local override path - cmd.Printf("Source: user override (%s)\n", overridePath) + cmd.Println(fmt.Sprintf("Source: user override (%s)", overridePath)) printTemplateVars(cmd, info) cmd.Println() cmd.Print(string(data)) @@ -211,7 +211,7 @@ func runMessageEdit(cmd *cobra.Command, hook, variant string) error { return fmt.Errorf("failed to write override %s: %w", oPath, writeErr) } - cmd.Printf("Override created at %s\n", oPath) + cmd.Println(fmt.Sprintf("Override created at %s", oPath)) cmd.Println("Edit this file to customize the message.") printTemplateVars(cmd, info) @@ -240,7 +240,7 @@ func runMessageReset(cmd *cobra.Command, hook, variant string) error { if removeErr := os.Remove(oPath); removeErr != nil { if os.IsNotExist(removeErr) { - cmd.Printf("No override found for %s/%s. Already using embedded default.\n", hook, variant) + cmd.Println(fmt.Sprintf("No override found for %s/%s. Already using embedded default.", hook, variant)) return nil } return fmt.Errorf("failed to remove override %s: %w", oPath, removeErr) @@ -252,7 +252,7 @@ func runMessageReset(cmd *cobra.Command, hook, variant string) error { messagesDir := filepath.Dir(hookDir) _ = os.Remove(messagesDir) // only succeeds if empty - cmd.Printf("Override removed for %s/%s. Using embedded default.\n", hook, variant) + cmd.Println(fmt.Sprintf("Override removed for %s/%s. Using embedded default.", hook, variant)) return nil } @@ -286,5 +286,5 @@ func printTemplateVars(cmd *cobra.Command, info *messages.HookMessageInfo) { for i, v := range info.TemplateVars { formatted[i] = "{{." + v + "}}" } - cmd.Printf("Template variables: %s\n", strings.Join(formatted, ", ")) + cmd.Println(fmt.Sprintf("Template variables: %s", strings.Join(formatted, ", "))) } diff --git a/internal/cli/system/pause.go b/internal/cli/system/pause.go index bae697e4..317d5b94 100644 --- a/internal/cli/system/pause.go +++ b/internal/cli/system/pause.go @@ -7,6 +7,7 @@ package system import ( + "fmt" "os" "github.com/spf13/cobra" @@ -46,6 +47,6 @@ func runPause(cmd *cobra.Command, stdin *os.File) error { path := pauseMarkerPath(sessionID) writeCounter(path, 0) - cmd.Printf("Context hooks paused for session %s\n", sessionID) + cmd.Println(fmt.Sprintf("Context hooks paused for session %s", sessionID)) return nil } diff --git a/internal/cli/system/resume.go b/internal/cli/system/resume.go index 7f21ebe8..75918502 100644 --- a/internal/cli/system/resume.go +++ b/internal/cli/system/resume.go @@ -7,6 +7,7 @@ package system import ( + "fmt" "os" "github.com/spf13/cobra" @@ -45,6 +46,6 @@ func runResume(cmd *cobra.Command, stdin *os.File) error { path := pauseMarkerPath(sessionID) _ = os.Remove(path) - cmd.Printf("Context hooks resumed for session %s\n", sessionID) + cmd.Println(fmt.Sprintf("Context hooks resumed for session %s", sessionID)) return nil } diff --git a/internal/cli/system/session_tokens.go b/internal/cli/system/session_tokens.go index 8a4a578c..1d910de9 100644 --- a/internal/cli/system/session_tokens.go +++ b/internal/cli/system/session_tokens.go @@ -10,6 +10,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/ActiveMemory/ctx/internal/config" "io" "os" "path/filepath" @@ -154,7 +155,7 @@ func parseLastUsageAndModel(path string) (sessionTokenInfo, error) { } // Scan lines in reverse for the last assistant message with usage - lines := bytes.Split(tail, []byte("\n")) + lines := bytes.Split(tail, []byte(config.NewlineLF)) for i := len(lines) - 1; i >= 0; i-- { line := bytes.TrimSpace(lines[i]) if len(line) == 0 { diff --git a/internal/cli/task/doc.go b/internal/cli/task/doc.go new file mode 100644 index 00000000..bdf44594 --- /dev/null +++ b/internal/cli/task/doc.go @@ -0,0 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package task implements the ctx tasks command for managing task archival +// and snapshots. +package task diff --git a/internal/cli/task/path.go b/internal/cli/task/path.go index 6f2c9b30..6c21a586 100644 --- a/internal/cli/task/path.go +++ b/internal/cli/task/path.go @@ -1,3 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + package task import ( diff --git a/internal/cli/watch/run.go b/internal/cli/watch/run.go index 1dd276e0..9c091fe7 100644 --- a/internal/cli/watch/run.go +++ b/internal/cli/watch/run.go @@ -59,7 +59,7 @@ func runWatch(cmd *cobra.Command, _ []string) error { defer func(file *os.File) { err := file.Close() if err != nil { - cmd.Printf("failed to close log file: %v\n", err) + cmd.Println(fmt.Sprintf("failed to close log file: %v", err)) } }(file) reader = file diff --git a/internal/cli/watch/stream.go b/internal/cli/watch/stream.go index d696a314..09bc585a 100644 --- a/internal/cli/watch/stream.go +++ b/internal/cli/watch/stream.go @@ -77,21 +77,21 @@ func processStream(cmd *cobra.Command, reader io.Reader) error { } if watchDryRun { - cmd.Printf( + cmd.Println(fmt.Sprintf( "%s Would apply: [%s] %s\n", yellow("○"), update.Type, update.Content, - ) + )) } else { err := applyUpdate(update) if err != nil { - cmd.Printf( + cmd.Println(fmt.Sprintf( "%s Failed to apply [%s]: %v\n", color.RedString("✗"), update.Type, err, - ) + )) } else { - cmd.Printf( + cmd.Println(fmt.Sprintf( "%s Applied: [%s] %s\n", green("✓"), update.Type, update.Content, - ) + )) updateCount++ } } diff --git a/internal/cli/why/run.go b/internal/cli/why/run.go index 8fffd83c..8d3545fa 100644 --- a/internal/cli/why/run.go +++ b/internal/cli/why/run.go @@ -38,7 +38,7 @@ func showMenu(cmd *cobra.Command) error { ctx -> why`) cmd.Println() for i, doc := range docOrder { - cmd.Printf(" [%d] %s\n", i+1, doc.label) + cmd.Println(fmt.Sprintf(" [%d] %s", i+1, doc.label)) } cmd.Print("\nSelect a document (1-3): ") diff --git a/internal/cli/why/strip.go b/internal/cli/why/strip.go index 0925e005..801c0fa1 100644 --- a/internal/cli/why/strip.go +++ b/internal/cli/why/strip.go @@ -7,6 +7,7 @@ package why import ( + "github.com/ActiveMemory/ctx/internal/config" "regexp" "strings" ) @@ -33,7 +34,7 @@ var imageRe = regexp.MustCompile(`^\s*!\[.*\]\(.*\)\s*$`) // Returns: // - string: Cleaned Markdown suitable for terminal display func StripMkDocs(content string) string { - lines := strings.Split(content, "\n") + lines := strings.Split(content, config.NewlineLF) var result []string // Strip YAML frontmatter. @@ -107,7 +108,7 @@ func StripMkDocs(content string) string { result = append(result, line) } - return strings.Join(result, "\n") + return strings.Join(result, config.NewlineLF) } // extractAdmonitionTitle pulls the quoted title from an admonition line. diff --git a/internal/compliance/compliance_test.go b/internal/compliance/compliance_test.go new file mode 100644 index 00000000..2a4d2099 --- /dev/null +++ b/internal/compliance/compliance_test.go @@ -0,0 +1,865 @@ +// / Context: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package compliance contains tests that verify the entire codebase adheres +// to the project standards documented in CONTRIBUTING.md, CLAUDE.md, and +// the lint-drift / lint-docs scripts. +// +// These tests are cross-cutting: they inspect source files, configs, and +// build artifacts across the whole repository rather than testing a single +// package's exported API. They mirror the checks performed by +// hack/lint-drift.sh and hack/lint-docs.sh so that violations surface in +// `go test` without requiring bash. +package compliance + +import ( + "bufio" + "bytes" + "go/ast" + "go/format" + "go/parser" + "go/token" + "os" + "os/exec" + "path/filepath" + "regexp" + "strings" + "testing" +) + +// projectRoot returns the absolute path to the project root. +func projectRoot(t *testing.T) string { + t.Helper() + root, err := filepath.Abs(filepath.Join("..", "..")) + if err != nil { + t.Fatalf("failed to resolve project root: %v", err) + } + return root +} + +// templateDir returns the path to the template/assets directory. +// It supports both internal/assets (current) and internal/tpl (legacy). +func templateDir(t *testing.T, root string) string { + t.Helper() + for _, name := range []string{"assets", "tpl"} { + d := filepath.Join(root, "internal", name) + if _, err := os.Stat(d); err == nil { + return d + } + } + t.Fatal("cannot find template dir (internal/assets or internal/tpl)") + return "" +} + +// allGoFiles returns all .go files under the project root, excluding vendor/. +func allGoFiles(t *testing.T, root string) []string { + t.Helper() + var files []string + err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() && (info.Name() == "vendor" || info.Name() == ".git" || info.Name() == "dist" || info.Name() == "site") { + return filepath.SkipDir + } + if !info.IsDir() && strings.HasSuffix(path, ".go") { + files = append(files, path) + } + return nil + }) + if err != nil { + t.Fatalf("failed to walk project: %v", err) + } + return files +} + +// nonTestGoFiles returns all non-test .go files. +func nonTestGoFiles(t *testing.T, root string) []string { + t.Helper() + var result []string + for _, f := range allGoFiles(t, root) { + if !strings.HasSuffix(f, "_test.go") { + result = append(result, f) + } + } + return result +} + +// --------------------------------------------------------------------------- +// 1. License Header ╬ô├ç├╢ every .go file must have the SPDX header +// --------------------------------------------------------------------------- + +// TestLicenseHeader verifies every .go file contains the Apache-2.0 SPDX +// identifier within the first 10 lines. +func TestLicenseHeader(t *testing.T) { + root := projectRoot(t) + spdxTag := "SPDX-License-Identifier: Apache-2.0" + + for _, p := range allGoFiles(t, root) { + rel, _ := filepath.Rel(root, p) + t.Run(rel, func(t *testing.T) { + data, err := os.ReadFile(filepath.Clean(p)) + if err != nil { + t.Fatalf("read: %v", err) + } + scanner := bufio.NewScanner(strings.NewReader(string(data))) + found := false + for i := 0; i < 10 && scanner.Scan(); i++ { + if strings.Contains(scanner.Text(), spdxTag) { + found = true + break + } + } + if !found { + t.Errorf("missing SPDX license header (%s)", spdxTag) + } + }) + } +} + +// --------------------------------------------------------------------------- +// 2. Package doc.go ╬ô├ç├╢ every package under internal/ should have a doc.go +// --------------------------------------------------------------------------- + +// TestDocGoExists verifies every Go package under internal/ has a doc.go. +// +// Some packages (like cli) use sub-packages; for those the check recurses +// into each sub-package directory instead. +func TestDocGoExists(t *testing.T) { + root := projectRoot(t) + internalDir := filepath.Join(root, "internal") + + // Packages exempt from the doc.go requirement. + exempt := map[string]bool{ + "compliance": true, // this test-only package + } + + entries, err := os.ReadDir(internalDir) + if err != nil { + t.Fatalf("readdir internal/: %v", err) + } + + for _, e := range entries { + if !e.IsDir() || exempt[e.Name()] { + continue + } + + pkgDir := filepath.Join(internalDir, e.Name()) + + // If the package contains sub-packages (directories with .go files), + // check each sub-package instead of the parent. + subEntries, subErr := os.ReadDir(pkgDir) + if subErr != nil { + t.Fatalf("readdir internal/%s: %v", e.Name(), subErr) + } + + hasSubPkgs := false + for _, sub := range subEntries { + if sub.IsDir() { + subDir := filepath.Join(pkgDir, sub.Name()) + goFiles, _ := filepath.Glob(filepath.Join(subDir, "*.go")) + if len(goFiles) > 0 { + hasSubPkgs = true + name := e.Name() + "/" + sub.Name() + t.Run(name, func(t *testing.T) { + docPath := filepath.Join(subDir, "doc.go") + if _, statErr := os.Stat(docPath); os.IsNotExist(statErr) { + t.Errorf("missing doc.go in internal/%s", name) + } + }) + } + } + } + + // If no sub-packages, check the package itself. + if !hasSubPkgs { + t.Run(e.Name(), func(t *testing.T) { + docPath := filepath.Join(pkgDir, "doc.go") + if _, statErr := os.Stat(docPath); os.IsNotExist(statErr) { + t.Errorf("missing doc.go in internal/%s", e.Name()) + } + }) + } + } +} + +// --------------------------------------------------------------------------- +// 3. No literal "\n" ╬ô├ç├╢ use config.NewlineLF instead (lint-drift rule 1) +// --------------------------------------------------------------------------- + +// TestNoLiteralNewline mirrors lint-drift rule 1: literal "\n" strings +// should use config.NewlineLF instead. +func TestNoLiteralNewline(t *testing.T) { + root := projectRoot(t) + re := regexp.MustCompile(`"\\n"`) + + for _, p := range nonTestGoFiles(t, root) { + if strings.HasSuffix(p, "token.go") { + continue + } + rel, _ := filepath.Rel(root, p) + + data, err := os.ReadFile(filepath.Clean(p)) + if err != nil { + t.Fatalf("read %s: %v", rel, err) + } + if re.Match(data) { + t.Run(rel, func(t *testing.T) { + t.Errorf("literal \"\\n\" found, want config.NewlineLF") + }) + } + } +} + +// --------------------------------------------------------------------------- +// 4. No literal ".md" ╬ô├ç├╢ use config.ExtMarkdown instead (lint-drift rule 4) +// --------------------------------------------------------------------------- + +// TestNoLiteralMdExtension mirrors lint-drift rule 4: literal ".md" strings +// should use config.ExtMarkdown instead. +func TestNoLiteralMdExtension(t *testing.T) { + root := projectRoot(t) + re := regexp.MustCompile(`"\.md"`) + + for _, p := range nonTestGoFiles(t, root) { + if strings.HasSuffix(p, filepath.Join("config", "file.go")) { + continue + } + rel, _ := filepath.Rel(root, p) + + data, err := os.ReadFile(filepath.Clean(p)) + if err != nil { + t.Fatalf("read %s: %v", rel, err) + } + if re.Match(data) { + t.Run(rel, func(t *testing.T) { + t.Errorf("literal \".md\" found, want config.ExtMarkdown") + }) + } + } +} + +// --------------------------------------------------------------------------- +// 5. No cmd.Printf/cmd.PrintErrf ╬ô├ç├╢ prefer Println (lint-drift rule 2) +// --------------------------------------------------------------------------- + +// TestNoCmdPrintf mirrors lint-drift rule 2: cmd.Printf/cmd.PrintErrf should +// be replaced with cmd.Println(fmt.Sprintf(...)). +func TestNoCmdPrintf(t *testing.T) { + root := projectRoot(t) + re := regexp.MustCompile(`cmd\.(Printf|PrintErrf)\(`) + + for _, p := range nonTestGoFiles(t, root) { + rel, _ := filepath.Rel(root, p) + + data, err := os.ReadFile(filepath.Clean(p)) + if err != nil { + t.Fatalf("read %s: %v", rel, err) + } + if re.Match(data) { + t.Run(rel, func(t *testing.T) { + t.Errorf("cmd.Printf/PrintErrf found, want cmd.Println(fmt.Sprintf(...))") + }) + } + } +} + +// --------------------------------------------------------------------------- +// 6. No magic directory strings ╬ô├ç├╢ use config.Dir* constants (lint-drift rule 3) +// --------------------------------------------------------------------------- + +// TestNoMagicDirectoryStrings mirrors lint-drift rule 3: magic directory +// strings in filepath.Join calls should use config.Dir* constants. +func TestNoMagicDirectoryStrings(t *testing.T) { + root := projectRoot(t) + + tests := []struct { + pattern string + constant string + }{ + {`filepath\.Join\([^)]*"sessions"`, "config.DirSessions"}, + {`filepath\.Join\([^)]*"archive"`, "config.DirArchive"}, + {`filepath\.Join\([^)]*"tools"`, "config.DirTools"}, + } + + for _, tt := range tests { + re := regexp.MustCompile(tt.pattern) + for _, p := range nonTestGoFiles(t, root) { + rel, _ := filepath.Rel(root, p) + + data, err := os.ReadFile(filepath.Clean(p)) + if err != nil { + t.Fatalf("read %s: %v", rel, err) + } + if re.Match(data) { + t.Run(rel+"/"+tt.constant, func(t *testing.T) { + t.Errorf("magic directory string found, want %s", tt.constant) + }) + } + } + } +} + +// --------------------------------------------------------------------------- +// 7. No direct fmt.Print* in Cobra command functions ╬ô├ç├╢ use cmd.Print* +// --------------------------------------------------------------------------- + +// TestNoDirectFmtPrintInCobraHandlers parses CLI source files and verifies +// that functions accepting *cobra.Command do not call fmt.Print* directly. +// Output should go through cmd.Print* so tests can capture it and --quiet +// flags work correctly. +func TestNoDirectFmtPrintInCobraHandlers(t *testing.T) { + root := projectRoot(t) + cliDir := filepath.Join(root, "internal", "cli") + + forbidden := map[string]bool{ + "Print": true, + "Println": true, + "Printf": true, + } + + err := filepath.Walk(cliDir, func(path string, info os.FileInfo, walkErr error) error { + if walkErr != nil { + return walkErr + } + if info.IsDir() || !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") { + return nil + } + + fset := token.NewFileSet() + node, parseErr := parser.ParseFile(fset, path, nil, parser.ParseComments) + if parseErr != nil { + t.Errorf("parse %s: %v", path, parseErr) + return nil + } + + // Check if file imports "fmt" + var fmtAlias string + for _, imp := range node.Imports { + impPath := strings.Trim(imp.Path.Value, `"`) + if impPath == "fmt" { + if imp.Name != nil { + fmtAlias = imp.Name.Name + } else { + fmtAlias = "fmt" + } + break + } + } + if fmtAlias == "" { + return nil + } + + for _, decl := range node.Decls { + fn, ok := decl.(*ast.FuncDecl) + if !ok || fn.Type.Params == nil { + continue + } + + hasCobraCmd := false + for _, param := range fn.Type.Params.List { + if star, ok := param.Type.(*ast.StarExpr); ok { + if sel, ok := star.X.(*ast.SelectorExpr); ok { + if sel.Sel.Name == "Command" { + hasCobraCmd = true + break + } + } + } + } + if !hasCobraCmd { + continue + } + + ast.Inspect(fn.Body, func(n ast.Node) bool { + call, ok := n.(*ast.CallExpr) + if !ok { + return true + } + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return true + } + ident, ok := sel.X.(*ast.Ident) + if !ok { + return true + } + if ident.Name == fmtAlias && forbidden[sel.Sel.Name] { + pos := fset.Position(call.Pos()) + rel, _ := filepath.Rel(root, pos.Filename) + t.Errorf("%s:%d: fmt.%s in Cobra handler ╬ô├ç├╢ use cmd.Print* instead", + rel, pos.Line, sel.Sel.Name) + } + return true + }) + } + return nil + }) + if err != nil { + t.Fatalf("walk cli dir: %v", err) + } +} + +// --------------------------------------------------------------------------- +// 8. gofmt compliance ╬ô├ç├╢ all Go files must be properly formatted +// --------------------------------------------------------------------------- + +// TestGofmt verifies all Go files are properly formatted. +// It normalizes CRLF to LF before comparison so that the test passes on +// Windows where git may check out files with CRLF line endings. +func TestGofmt(t *testing.T) { + if testing.Short() { + t.Skip("skipping gofmt check in short mode") + } + + root := projectRoot(t) + + var unformatted []string + for _, f := range allGoFiles(t, root) { + data, err := os.ReadFile(filepath.Clean(f)) + if err != nil { + t.Fatalf("read %s: %v", f, err) + } + // Normalize CRLF to LF so the check works on Windows. + normalized := bytes.ReplaceAll(data, []byte("\r\n"), []byte("\n")) + formatted, fmtErr := format.Source(normalized) + if fmtErr != nil { + // File doesn't parse; go vet will catch it. + continue + } + if !bytes.Equal(normalized, formatted) { + rel, _ := filepath.Rel(root, f) + unformatted = append(unformatted, rel) + } + } + + if len(unformatted) > 0 { + t.Errorf("files need formatting:\n\t%s\n\nRun: go fmt ./...", + strings.Join(unformatted, "\n\t")) + } +} + +// --------------------------------------------------------------------------- +// 9. go vet ╬ô├ç├╢ the entire project must pass go vet +// --------------------------------------------------------------------------- + +// TestGoVet runs go vet across the entire project with CGO disabled. +func TestGoVet(t *testing.T) { + if testing.Short() { + t.Skip("skipping go vet in short mode") + } + + root := projectRoot(t) + + cmd := exec.Command("go", "vet", "./...") + cmd.Dir = root + cmd.Env = append(os.Environ(), "CGO_ENABLED=0") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("go vet failed:\n%s", string(output)) + } +} + +// --------------------------------------------------------------------------- +// 9b. golangci-lint ╬ô├ç├╢ the entire project must pass golangci-lint +// --------------------------------------------------------------------------- + +// TestGolangciLint runs golangci-lint across the entire project. +// This catches issues that go vet alone misses (gosec, goconst, unused, etc.). +// golangci-lint is a required dependency ╬ô├ç├╢ the test fails if it is not installed. +func TestGolangciLint(t *testing.T) { + if testing.Short() { + t.Skip("skipping golangci-lint in short mode") + } + + // golangci-lint may not be installed in every CI job (the Lint job + // runs it separately via golangci-lint-action). Skip gracefully. + if _, err := exec.LookPath("golangci-lint"); err != nil { + t.Skip("golangci-lint is not installed.\n" + + "Install it with:\n" + + " go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.8.0\n" + + "Or see: https://golangci-lint.run/welcome/install/") + } + + root := projectRoot(t) + + cmd := exec.Command("golangci-lint", "run", "--timeout=5m") + cmd.Dir = root + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("golangci-lint failed:\n%s", string(output)) + } +} + +// --------------------------------------------------------------------------- +// 10. No secrets in .context/ templates ╬ô├ç├╢ no tokens, keys, passwords +// --------------------------------------------------------------------------- + +// TestNoSecretsInTemplates scans template files for patterns that look like +// secrets (API keys, tokens, private keys) per SECURITY.md requirements. +func TestNoSecretsInTemplates(t *testing.T) { + root := projectRoot(t) + tplDir := templateDir(t, root) + + secretPatterns := []*regexp.Regexp{ + regexp.MustCompile(`(?i)(api[_-]?key|secret[_-]?key|password|token|credential)\s*[:=]`), + regexp.MustCompile(`(?i)(sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36}|gho_[a-zA-Z0-9]{36})`), + regexp.MustCompile(`(?i)-----BEGIN (RSA |EC )?PRIVATE KEY-----`), + } + + err := filepath.Walk(tplDir, func(path string, info os.FileInfo, walkErr error) error { + if walkErr != nil || info.IsDir() { + return walkErr + } + + data, readErr := os.ReadFile(filepath.Clean(path)) //nolint:gosec // path comes from filepath.Walk, not user input + if readErr != nil { + t.Errorf("read %s: %v", path, readErr) + return nil + } + + rel, _ := filepath.Rel(root, path) + for _, re := range secretPatterns { + if re.Match(data) { + t.Errorf("%s: potential secret pattern found: %s", rel, re.String()) + } + } + return nil + }) + if err != nil { + t.Fatalf("walk assets dir: %v", err) + } +} + +// --------------------------------------------------------------------------- +// 11. Required context files ╬ô├ç├╢ ctx init must create all required files +// --------------------------------------------------------------------------- + +// TestRequiredContextFilesInTemplate verifies that all required context file +// templates exist in internal/assets/ so that ctx init can scaffold them. +func TestRequiredContextFilesInTemplate(t *testing.T) { + root := projectRoot(t) + tplDir := templateDir(t, root) + + requiredFiles := []string{ + "CONSTITUTION.md", + "TASKS.md", + "DECISIONS.md", + "LEARNINGS.md", + "CONVENTIONS.md", + "ARCHITECTURE.md", + } + + for _, name := range requiredFiles { + t.Run(name, func(t *testing.T) { + // Templates live under context/ subdirectory in assets. + path := filepath.Join(tplDir, "context", name) + if _, err := os.Stat(path); os.IsNotExist(err) { + t.Errorf("required template %s not found in internal/assets/context/", name) + } + }) + } +} + +// --------------------------------------------------------------------------- +// 12. VERSION file ╬ô├ç├╢ must exist and contain a valid semver +// --------------------------------------------------------------------------- + +// TestVersionFile checks the VERSION file exists and contains valid semver. +func TestVersionFile(t *testing.T) { + root := projectRoot(t) + versionPath := filepath.Join(root, "VERSION") + + data, err := os.ReadFile(filepath.Clean(versionPath)) //nolint:gosec // constructed from test constants + if err != nil { + t.Fatalf("cannot read VERSION file: %v", err) + } + + version := strings.TrimSpace(string(data)) + if version == "" { + t.Fatal("VERSION file is empty") + } + + semverRe := regexp.MustCompile(`^\d+\.\d+\.\d+(-[a-zA-Z0-9.]+)?$`) + if !semverRe.MatchString(version) { + t.Errorf("VERSION %q is not valid semver (expected X.Y.Z)", version) + } +} + +// --------------------------------------------------------------------------- +// 13. go.mod ╬ô├ç├╢ module path and Go version check +// --------------------------------------------------------------------------- + +// TestGoMod verifies the module path and Go version in go.mod. +func TestGoMod(t *testing.T) { + root := projectRoot(t) + modPath := filepath.Join(root, "go.mod") + + data, err := os.ReadFile(filepath.Clean(modPath)) //nolint:gosec // constructed from test constants + if err != nil { + t.Fatalf("cannot read go.mod: %v", err) + } + + content := string(data) + + t.Run("module path", func(t *testing.T) { + if !strings.Contains(content, "module github.com/ActiveMemory/ctx") { + t.Error("go.mod should declare module github.com/ActiveMemory/ctx") + } + }) + + t.Run("go version declared", func(t *testing.T) { + goVersionRe := regexp.MustCompile(`go\s+1\.\d+`) + if !goVersionRe.MatchString(content) { + t.Error("go.mod should declare a Go version (go 1.x)") + } + }) +} + +// --------------------------------------------------------------------------- +// 14. Makefile ╬ô├ç├╢ required targets exist +// --------------------------------------------------------------------------- + +// TestMakefileTargets verifies all expected build targets exist in the Makefile. +func TestMakefileTargets(t *testing.T) { + root := projectRoot(t) + makePath := filepath.Join(root, "Makefile") + + data, err := os.ReadFile(filepath.Clean(makePath)) //nolint:gosec // constructed from test constants + if err != nil { + t.Fatalf("cannot read Makefile: %v", err) + } + + content := string(data) + + requiredTargets := []string{ + "build:", + "test:", + "vet:", + "fmt:", + "lint:", + "clean:", + } + + for _, target := range requiredTargets { + t.Run(strings.TrimSuffix(target, ":"), func(t *testing.T) { + if !strings.Contains(content, target) { + t.Errorf("Makefile missing required target: %s", target) + } + }) + } +} + +// --------------------------------------------------------------------------- +// 15. CGO_ENABLED=0 ╬ô├ç├╢ build command must not use CGO +// --------------------------------------------------------------------------- + +// TestBuildWithoutCGO verifies that Makefile build and test targets use +// CGO_ENABLED=0 as required by the project standards. +func TestBuildWithoutCGO(t *testing.T) { + root := projectRoot(t) + makePath := filepath.Join(root, "Makefile") + + data, err := os.ReadFile(filepath.Clean(makePath)) //nolint:gosec // constructed from test constants + if err != nil { + t.Fatalf("cannot read Makefile: %v", err) + } + + content := string(data) + + t.Run("build target uses CGO_ENABLED=0", func(t *testing.T) { + if !strings.Contains(content, "CGO_ENABLED=0") { + t.Error("Makefile build target should use CGO_ENABLED=0") + } + }) + + t.Run("test target uses CGO_ENABLED=0", func(t *testing.T) { + // Find the test target line and check CGO + lines := strings.Split(content, "\n") + for i, line := range lines { + if strings.HasPrefix(line, "test:") { + // Check the next few lines for CGO_ENABLED=0 + found := false + for j := i + 1; j < i+5 && j < len(lines); j++ { + if strings.Contains(lines[j], "CGO_ENABLED=0") { + found = true + break + } + } + if !found { + t.Error("test target should use CGO_ENABLED=0") + } + break + } + } + }) +} + +// --------------------------------------------------------------------------- +// 16. .golangci.yml ╬ô├ç├╢ required linters are configured +// --------------------------------------------------------------------------- + +// TestGolangciLintConfig verifies that .golangci.yml enables the required +// linters (govet, errcheck, staticcheck, gosec). +func TestGolangciLintConfig(t *testing.T) { + root := projectRoot(t) + lintPath := filepath.Join(root, ".golangci.yml") + + data, err := os.ReadFile(filepath.Clean(lintPath)) //nolint:gosec // constructed from test constants + if err != nil { + t.Fatalf("cannot read .golangci.yml: %v", err) + } + + content := string(data) + + requiredLinters := []string{ + "govet", + "errcheck", + "staticcheck", + "gosec", + } + + for _, linter := range requiredLinters { + t.Run(linter, func(t *testing.T) { + if !strings.Contains(content, linter) { + t.Errorf(".golangci.yml missing required linter: %s", linter) + } + }) + } +} + +// --------------------------------------------------------------------------- +// 17. No network calls ╬ô├ç├╢ ctx must be local-only (no net/http imports in core) +// --------------------------------------------------------------------------- + +// TestNoNetworkImportsInCore verifies that core packages do not import net or +// net/http, enforcing the local-only design described in SECURITY.md. +func TestNoNetworkImportsInCore(t *testing.T) { + root := projectRoot(t) + + localOnlyPackages := []string{ + "context", + "config", + "drift", + "task", + "validation", + "crypto", + "assets", + "index", + } + + for _, pkg := range localOnlyPackages { + pkgDir := filepath.Join(root, "internal", pkg) + if _, err := os.Stat(pkgDir); os.IsNotExist(err) { + continue + } + + t.Run(pkg, func(t *testing.T) { + fset := token.NewFileSet() + pkgs, parseErr := parser.ParseDir(fset, pkgDir, func(info os.FileInfo) bool { + return !strings.HasSuffix(info.Name(), "_test.go") + }, parser.ImportsOnly) + if parseErr != nil { + t.Fatalf("parse %s: %v", pkg, parseErr) + } + + for _, p := range pkgs { + for _, f := range p.Files { + for _, imp := range f.Imports { + impPath := strings.Trim(imp.Path.Value, `"`) + if impPath == "net/http" || impPath == "net" { + pos := fset.Position(imp.Pos()) + t.Errorf("%s:%d: %s imports %q ╬ô├ç├╢ ctx core must be local-only", + filepath.Base(pos.Filename), pos.Line, pkg, impPath) + } + } + } + } + }) + } +} + +// --------------------------------------------------------------------------- +// 18. Security ╬ô├ç├╢ .gitignore protects sensitive files +// --------------------------------------------------------------------------- + +// TestGitignoreProtectsSensitiveFiles ensures .gitignore contains entries for +// files that must never be committed (encryption keys, etc.). +func TestGitignoreProtectsSensitiveFiles(t *testing.T) { + root := projectRoot(t) + giPath := filepath.Join(root, ".gitignore") + + data, err := os.ReadFile(filepath.Clean(giPath)) //nolint:gosec // constructed from test constants + if err != nil { + t.Fatalf("cannot read .gitignore: %v", err) + } + + content := string(data) + + sensitivePatterns := []string{ + ".scratchpad.key", + } + + for _, pattern := range sensitivePatterns { + t.Run(pattern, func(t *testing.T) { + if !strings.Contains(content, pattern) { + t.Errorf(".gitignore should protect %s", pattern) + } + }) + } +} + +// --------------------------------------------------------------------------- +// 19. Binary build ╬ô├ç├╢ ensure the project compiles without errors +// --------------------------------------------------------------------------- + +// TestProjectCompiles builds the entire project with CGO disabled to verify +// there are no compilation errors. +func TestProjectCompiles(t *testing.T) { + if testing.Short() { + t.Skip("skipping build test in short mode") + } + + root := projectRoot(t) + + cmd := exec.Command("go", "build", "./...") + cmd.Dir = root + cmd.Env = append(os.Environ(), "CGO_ENABLED=0") + output, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("project does not compile:\n%s", string(output)) + } +} + +// --------------------------------------------------------------------------- +// 20. File permissions ╬ô├ç├╢ config.PermSecret must be 0600 +// --------------------------------------------------------------------------- + +// TestPermissionConstants verifies that config.PermSecret and config.PermFile +// use the expected permission values. +func TestPermissionConstants(t *testing.T) { + root := projectRoot(t) + filePath := filepath.Join(root, "internal", "config", "file.go") + + data, err := os.ReadFile(filepath.Clean(filePath)) //nolint:gosec // constructed from test constants + if err != nil { + t.Fatalf("read file.go: %v", err) + } + + content := string(data) + + t.Run("PermSecret is 0600", func(t *testing.T) { + if !strings.Contains(content, "0600") { + t.Error("config.PermSecret should be 0600 for secret files") + } + }) + + t.Run("PermFile is 0644", func(t *testing.T) { + if !strings.Contains(content, "0644") { + t.Error("config.PermFile should be 0644 for regular files") + } + }) +} diff --git a/internal/config/entry.go b/internal/config/entry.go index c78a76ba..1d703a54 100644 --- a/internal/config/entry.go +++ b/internal/config/entry.go @@ -2,7 +2,7 @@ // ,'`./ do you remember? // `.,'\ // \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2. +// SPDX-License-Identifier: Apache-2.0 package config diff --git a/internal/config/field.go b/internal/config/field.go index ce098514..aa737781 100644 --- a/internal/config/field.go +++ b/internal/config/field.go @@ -4,7 +4,7 @@ // // `.,'\ // \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2. +// SPDX-License-Identifier: Apache-2.0 package config diff --git a/internal/config/file.go b/internal/config/file.go index 5e1a3755..5f010539 100644 --- a/internal/config/file.go +++ b/internal/config/file.go @@ -300,4 +300,3 @@ var Packages = map[string]string{ "requirements.txt": "Python dependencies", "Gemfile": "Ruby dependencies", } - diff --git a/internal/config/heading.go b/internal/config/heading.go index 8d7b9062..9d62dca4 100644 --- a/internal/config/heading.go +++ b/internal/config/heading.go @@ -4,7 +4,7 @@ // // `.,'\ // \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2. +// SPDX-License-Identifier: Apache-2.0 package config diff --git a/internal/config/pattern.go b/internal/config/pattern.go index bad04a49..3d8fb66b 100644 --- a/internal/config/pattern.go +++ b/internal/config/pattern.go @@ -2,7 +2,7 @@ // ,'`./ do you remember? // `.,'\ // \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2. +// SPDX-License-Identifier: Apache-2.0 package config diff --git a/internal/config/token.go b/internal/config/token.go index 66bda4e7..2c289869 100644 --- a/internal/config/token.go +++ b/internal/config/token.go @@ -2,7 +2,7 @@ // ,'`./ do you remember? // `.,'\ // \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2. +// SPDX-License-Identifier: Apache-2.0 package config diff --git a/internal/context/doc.go b/internal/context/doc.go new file mode 100644 index 00000000..0893f20d --- /dev/null +++ b/internal/context/doc.go @@ -0,0 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package context loads and manages .context/ files with token counting +// and metadata. +package context diff --git a/internal/context/types.go b/internal/context/types.go index 05378546..f4c0167e 100644 --- a/internal/context/types.go +++ b/internal/context/types.go @@ -2,7 +2,7 @@ // ,'`./ do you remember? // `.,'\ // \ Copyright 2026-present Context contributors. -// SPDX-License-Identifier: Apache-2. +// SPDX-License-Identifier: Apache-2.0 package context diff --git a/internal/crypto/doc.go b/internal/crypto/doc.go new file mode 100644 index 00000000..962598da --- /dev/null +++ b/internal/crypto/doc.go @@ -0,0 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package crypto provides AES-256-GCM encryption and decryption for the +// scratchpad. +package crypto diff --git a/internal/drift/doc.go b/internal/drift/doc.go new file mode 100644 index 00000000..d5648ff2 --- /dev/null +++ b/internal/drift/doc.go @@ -0,0 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package drift detects stale or invalid context files and structural +// violations. +package drift diff --git a/internal/drift/types.go b/internal/drift/types.go index 5f7c7729..8fa5a02d 100644 --- a/internal/drift/types.go +++ b/internal/drift/types.go @@ -1,3 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + package drift // IssueType categorizes a drift issue for grouping and filtering. diff --git a/internal/eventlog/doc.go b/internal/eventlog/doc.go new file mode 100644 index 00000000..8c2aa7a1 --- /dev/null +++ b/internal/eventlog/doc.go @@ -0,0 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package eventlog provides append-only JSONL event logging for hook +// diagnostics. +package eventlog diff --git a/internal/index/doc.go b/internal/index/doc.go new file mode 100644 index 00000000..e83b4ef1 --- /dev/null +++ b/internal/index/doc.go @@ -0,0 +1,8 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package index generates and parses indexes for context file entries. +package index diff --git a/internal/journal/state/doc.go b/internal/journal/state/doc.go new file mode 100644 index 00000000..09da4c20 --- /dev/null +++ b/internal/journal/state/doc.go @@ -0,0 +1,8 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package state manages journal processing state via an external JSON file. +package state diff --git a/internal/notify/doc.go b/internal/notify/doc.go new file mode 100644 index 00000000..933d11b8 --- /dev/null +++ b/internal/notify/doc.go @@ -0,0 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package notify provides fire-and-forget webhook notifications with +// payload tracking. +package notify diff --git a/internal/rc/doc.go b/internal/rc/doc.go new file mode 100644 index 00000000..3020ca3a --- /dev/null +++ b/internal/rc/doc.go @@ -0,0 +1,8 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package rc loads and manages runtime configuration from .ctxrc files. +package rc diff --git a/internal/recall/parser/doc.go b/internal/recall/parser/doc.go new file mode 100644 index 00000000..6a9cd31e --- /dev/null +++ b/internal/recall/parser/doc.go @@ -0,0 +1,8 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package parser auto-detects and parses session files from multiple tools. +package parser diff --git a/internal/sysinfo/memory_darwin.go b/internal/sysinfo/memory_darwin.go index f6b26e67..3aedb399 100644 --- a/internal/sysinfo/memory_darwin.go +++ b/internal/sysinfo/memory_darwin.go @@ -9,6 +9,7 @@ package sysinfo import ( + "github.com/ActiveMemory/ctx/internal/config" "os/exec" "strconv" "strings" @@ -54,7 +55,7 @@ func parseVMStat(output string, totalBytes uint64) uint64 { var pageSize uint64 = 16384 // default on Apple Silicon pages := make(map[string]uint64) - for _, line := range strings.Split(output, "\n") { + for _, line := range strings.Split(output, config.NewlineLF) { if strings.Contains(line, "page size of") { for _, word := range strings.Fields(line) { if n, err := strconv.ParseUint(word, 10, 64); err == nil && n > 0 { diff --git a/internal/task/doc.go b/internal/task/doc.go new file mode 100644 index 00000000..7037a220 --- /dev/null +++ b/internal/task/doc.go @@ -0,0 +1,8 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + +// Package task provides task item parsing, matching, and domain logic. +package task diff --git a/internal/validation/path.go b/internal/validation/path.go index 23c28635..578726ff 100644 --- a/internal/validation/path.go +++ b/internal/validation/path.go @@ -1,3 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + package validation import ( diff --git a/internal/validation/path_test.go b/internal/validation/path_test.go index 346c6ab7..63d6add9 100644 --- a/internal/validation/path_test.go +++ b/internal/validation/path_test.go @@ -1,3 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + package validation import ( diff --git a/internal/validation/validate.go b/internal/validation/validate.go index da2b266e..bbe33828 100644 --- a/internal/validation/validate.go +++ b/internal/validation/validate.go @@ -1,3 +1,9 @@ +// / ctx: https://ctx.ist +// ,'`./ do you remember? +// `.,'\ +// \ Copyright 2026-present Context contributors. +// SPDX-License-Identifier: Apache-2.0 + package validation import (