1
0

2 کامیت‌ها 2032b74761 ... 1f1a55a670

نویسنده SHA1 پیام تاریخ
  Paul Klumpp 1f1a55a670 cli: configurable date formats (chatto + CLI), ISO for JSON/MCP; tests & docs 2 روز پیش
  Paul Klumpp 2032b74761 cli: configurable date formats (chatto + CLI), ISO for JSON/MCP; tests & docs 2 روز پیش
7فایلهای تغییر یافته به همراه155 افزوده شده و 7 حذف شده
  1. 23 5
      .env.example
  2. 3 0
      AGENTS.md
  3. 5 0
      README.md
  4. 1 1
      cmd/cheapo/chatto.go
  5. 80 0
      cmd/cheapo/datefmt.go
  6. 37 0
      cmd/cheapo/datefmt_test.go
  7. 6 1
      cmd/cheapo/main.go

+ 23 - 5
.env.example

@@ -2,19 +2,37 @@
 CHATTO_TOKEN="<token>"               # set by the operator
 # CHATTO_BASE_URL=https://chat.chatto.run   # optional, is the default
 
-# Date format for chatto-send output (Go time layout). Default: 02.01.2006 (unambiguous).
-# Go uses a reference time (Mon Jan 2 15:04:05 MST 2006) to describe the layout.
-# Examples:
+# Date format for chatto-send output. Default: 02.01.2006 (unambiguous).
+# Accepts Go time layouts (reference time Mon Jan 2 15:04:05 MST 2006)
+# OR common placeholders (dd, MM, yyyy, HH, mm, ss, ...):
+# Go layouts:
 #   02.01.2006          -> 31.12.2026
 #   2006-01-02          -> 2026-12-31
 #   02.01.2006 15:04    -> 31.12.2026 23:59
 #   January 2, 2006     -> December 31, 2026
+# Placeholders:
+#   dd.MM.yyyy          -> 31.12.2026
+#   yyyy-MM-dd          -> 2026-12-31
+#   dd.MM.yyyy HH:mm    -> 31.12.2026 23:59
 # Reference: https://pkg.go.dev/time#Time.Format
 # CHEAPO_CHATTO_OUTPUT_DATE_FORMAT=2006-01-02
+# CHEAPO_CHATTO_OUTPUT_DATE_FORMAT=dd.MM.yyyy
+# CHEAPO_CHATTO_OUTPUT_DATE_FORMAT=yyyy-MM-dd
 CHEAPO_CHATTO_OUTPUT_DATE_FORMAT=02.01.2006
 
-# Date format for CLI 'Free until:' output (Go time layout). Default: RFC3339 (ISO, unambiguous).
+# Date format for CLI 'Free until:' output. Default: RFC3339 (ISO, unambiguous).
 # JSON output and MCP always use ISO-8601 regardless of this setting.
+# Accepts Go time layouts OR common placeholders (dd, MM, yyyy, HH, mm, ss, ...).
+# Go layouts:
+#   2006-01-02                      -> 2026-12-31
+#   2006-01-02T15:04:05Z07:00      -> 2026-12-31T23:59:59Z
+# Placeholders:
+#   dd.MM.yyyy                      -> 31.12.2026
+#   yyyy-MM-dd                      -> 2026-12-31
+#   dd.MM.yyyy HH:mm                -> 31.12.2026 23:59
 # Default (ISO-8601 / RFC3339): 2006-01-02T15:04:05Z07:00
 # CHEAPO_CLI_OUTPUT_DATE_FORMAT=2006-01-02
-# CHEAPO_CLI_OUTPUT_DATE_FORMAT=2006-01-02T15:04:05Z07:00
+# CHEAPO_CLI_OUTPUT_DATE_FORMAT=2006-01-02T15:04:05Z07:00
+# CHEAPO_CLI_OUTPUT_DATE_FORMAT=dd.MM.yyyy
+# CHEAPO_CLI_OUTPUT_DATE_FORMAT=yyyy-MM-dd
+# CHEAPO_CLI_OUTPUT_DATE_FORMAT=dd.MM.yyyy HH:mm

+ 3 - 0
AGENTS.md

@@ -102,6 +102,9 @@ How it gets the info:
   messages and must be overridable via `CHEAPO_CHATTO_OUTPUT_DATE_FORMAT`. The CLI's human-
   readable "Free until:" line honors `CHEAPO_CLI_OUTPUT_DATE_FORMAT` (default
   RFC-3339). Avoid ambiguous `DD.MM.`/`MM/DD` outputs without a year.
+  Both `CHEAPO_CLI_OUTPUT_DATE_FORMAT` and `CHEAPO_CHATTO_OUTPUT_DATE_FORMAT`
+  accept either a Go time layout (`02.01.2006`) or common placeholders
+  (`dd.MM.yyyy`); see `.env.example`.
 - Wrap errors with context: `fmt.Errorf("epic: ...: %w", err)`.
 - New store platforms (Prime, GOG, …) follow the provider pattern (above).
 - **Chatto client:** `internal/chatto` is a hand-built Connect-JSON client

+ 5 - 0
README.md

@@ -161,6 +161,11 @@ CLI commands (credentials deliberately provided by the operator, never fetched a
 # .env in the project root (godotenv), never commit it — it is in .gitignore:
 CHATTO_TOKEN="<token>"               # set by the operator
 # CHATTO_BASE_URL=https://chat.chatto.run   # optional, is the default
+#
+# Date format for CLI 'Free until:' / chatto-send output (optional):
+#   CHEAPO_CLI_OUTPUT_DATE_FORMAT=dd.MM.yyyy
+#   CHEAPO_CHATTO_OUTPUT_DATE_FORMAT=dd.MM.yyyy
+# Accepts Go layouts (02.01.2006) or placeholders (dd, MM, yyyy, HH, mm, ss).
 
 cheapo chatto-rooms                     # list rooms (client verification)
 cheapo chatto-send --room <room_id>     # post current free games

+ 1 - 1
cmd/cheapo/chatto.go

@@ -64,7 +64,7 @@ func formatGames(games []game.Game, mode string) string {
 	b.WriteString("| Game | Store | Price | Period |\n")
 	b.WriteString("|---|---|---|---|\n")
 	for _, g := range games {
-		dateFmt := os.Getenv("CHEAPO_CHATTO_OUTPUT_DATE_FORMAT")
+		dateFmt := parseDateLayout(os.Getenv("CHEAPO_CHATTO_OUTPUT_DATE_FORMAT"))
 		if dateFmt == "" {
 			dateFmt = "02.01.2006"
 		}

+ 80 - 0
cmd/cheapo/datefmt.go

@@ -0,0 +1,80 @@
+package main
+
+import (
+	"strings"
+	"time"
+)
+
+// refTime is the canonical reference instant used to validate Go time layouts.
+var refTime = time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC)
+
+// parseDateLayout accepts either a Go time layout (e.g. "02.01.2006",
+// "2006-01-02") or a common placeholder format (e.g. "dd.MM.yyyy",
+// "yyyy-MM-dd HH:mm") and returns a Go time layout. An empty input yields
+// "" so the caller can apply its own default. Values that are neither a
+// valid Go layout nor a recognized placeholder string are returned
+// unchanged (the caller's Format call will then produce whatever Go does).
+func parseDateLayout(s string) string {
+	s = strings.TrimSpace(s)
+	if s == "" {
+		return ""
+	}
+	// If it already behaves like a Go layout, keep it as-is.
+	if isGoLayout(s) {
+		return s
+	}
+	return placeholdersToGoLayout(s)
+}
+
+// isGoLayout reports whether s is a usable Go time layout by round-tripping
+// a reference instant through Format and Parse.
+func isGoLayout(s string) bool {
+	formatted := refTime.Format(s)
+	parsed, err := time.Parse(s, formatted)
+	if err != nil {
+		return false
+	}
+	return parsed.UTC().Equal(refTime)
+}
+
+// placeholdersToGoLayout converts common date/time placeholders into the
+// equivalent Go time layout. Tokens are matched case-sensitively, longest
+// first, so that "MM" (month) is not confused with "mm" (minute).
+func placeholdersToGoLayout(s string) string {
+	// Ordered longest-first to avoid partial token matches.
+	replacements := []struct{ from, to string }{
+		{"yyyy", "2006"},
+		{"MMMM", "January"},
+		{"MMM", "Jan"},
+		{"MM", "01"},
+		{"dd", "02"},
+		{"HH", "15"},
+		{"mm", "04"},
+		{"ss", "05"},
+		{"yy", "06"},
+		{"M", "1"},
+		{"d", "2"},
+		{"H", "3"},
+		{"m", "4"},
+		{"s", "5"},
+	}
+	// Walk the string, consuming the longest matching token at each position.
+	var b strings.Builder
+	i := 0
+	for i < len(s) {
+		matched := false
+		for _, r := range replacements {
+			if strings.HasPrefix(s[i:], r.from) {
+				b.WriteString(r.to)
+				i += len(r.from)
+				matched = true
+				break
+			}
+		}
+		if !matched {
+			b.WriteByte(s[i])
+			i++
+		}
+	}
+	return b.String()
+}

+ 37 - 0
cmd/cheapo/datefmt_test.go

@@ -0,0 +1,37 @@
+package main
+
+import (
+	"testing"
+)
+
+func TestParseDateLayoutEmpty(t *testing.T) {
+	if got := parseDateLayout(""); got != "" {
+		t.Fatalf("expected empty, got %q", got)
+	}
+	if got := parseDateLayout("   "); got != "" {
+		t.Fatalf("expected empty, got %q", got)
+	}
+}
+
+func TestParseDateLayoutGoPassThrough(t *testing.T) {
+	cases := []string{"02.01.2006", "2006-01-02", "2006-01-02T15:04:05Z07:00", "02.01.2006 15:04"}
+	for _, c := range cases {
+		if got := parseDateLayout(c); got != c {
+			t.Errorf("Go layout %q changed to %q", c, got)
+		}
+	}
+}
+
+func TestParseDateLayoutPlaceholders(t *testing.T) {
+	cases := []struct{ in, want string }{
+		{"dd.MM.yyyy", "02.01.2006"},
+		{"yyyy-MM-dd", "2006-01-02"},
+		{"dd.MM.yyyy HH:mm", "02.01.2006 15:04"},
+		{"dd.MM.yyyy HH:mm:ss", "02.01.2006 15:04:05"},
+	}
+	for _, c := range cases {
+		if got := parseDateLayout(c.in); got != c.want {
+			t.Errorf("parseDateLayout(%q) = %q, want %q", c.in, got, c.want)
+		}
+	}
+}

+ 6 - 1
cmd/cheapo/main.go

@@ -30,6 +30,11 @@ var (
 )
 
 func main() {
+	// Load .env (godotenv) so values like CHEAPO_CLI_OUTPUT_DATE_FORMAT and
+	// CHEAPO_MCP_TOKEN apply to every command, not just mcp/chatto-*.
+	// Real environment variables take precedence over .env values.
+	loadEnv()
+
 	root := &cobra.Command{
 		Use:   "cheapo",
 		Short: "cheapo — free games lister (Go edition)",
@@ -113,7 +118,7 @@ func printGames(cmd *cobra.Command, games []game.Game) error {
 				g.StartDate.Local().Format("02.01.2006 15:04"),
 				g.EndDate.Local().Format("02.01.2006 15:04"))
 		} else if !g.EndDate.IsZero() {
-			fmtStr := os.Getenv("CHEAPO_CLI_OUTPUT_DATE_FORMAT")
+			fmtStr := parseDateLayout(os.Getenv("CHEAPO_CLI_OUTPUT_DATE_FORMAT"))
 			if fmtStr == "" {
 				fmtStr = time.RFC3339
 			}