Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bbdbffd789 | |||
| 88aa81ff46 | |||
| ad8f7937b1 | |||
| 3c2c3cef57 | |||
| cd2bd65b79 | |||
| dfb721117f | |||
| 37ad950241 | |||
| f1b44efca9 |
14 changed files with 316 additions and 16 deletions
11
.beads/.gitignore
vendored
Normal file
11
.beads/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Database
|
||||
*.db
|
||||
*.db-shm
|
||||
*.db-wal
|
||||
|
||||
# Lock files
|
||||
*.lock
|
||||
|
||||
# Temporary
|
||||
last-touched
|
||||
*.tmp
|
||||
1
.beads/config.yaml
Normal file
1
.beads/config.yaml
Normal file
|
|
@ -0,0 +1 @@
|
|||
issue_prefix: csv
|
||||
2
.beads/issues.jsonl
Normal file
2
.beads/issues.jsonl
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
{"id":"bd-3rr","title":"Fix -o flag: output filename not parsed correctly","description":"## Bug Report\n\nUsing the `-o` flag to specify an output filename causes errors. Instead of using the provided filename, the tool attempts to open/save `.xlsx` (empty basename) and prints memory addresses.\n\n## Reproduction\n\n```\ncsv2excel.exe -enc=windows1252 -o=test.xlsx .\\WDK-SiWo-20260103.csv\n```\n\n## Actual Output\n\n```\n0x3eb5d9d181b0\n0x3eb5d9d181c0\n0x3eb5d9d181d0\nFehler beim Öffnen .xlsx: open .xlsx: The system cannot find the file specified.\n✓ .\\WDK-SiWo-20260103.csv → Reiter \"WDK-SiWo-20260103\" (5324 Zeilen)\nFehler beim Speichern: unsupported workbook file format\n```\n\n## Analysis\n\n- Memory addresses being printed suggest a pointer/value is being passed where a string is expected (e.g. `fmt.Println(&flag)` instead of `fmt.Println(*flag)`)\n- The output path resolves to `.xlsx` instead of `test.xlsx`, meaning the flag value is not read correctly\n- The `unsupported workbook file format` error is a consequence of trying to open a non-existent file as a workbook\n\n## Acceptance Criteria\n- [ ] `csv2excel -o=test.xlsx input.csv` writes output to `test.xlsx`\n- [ ] No memory addresses are printed to stdout/stderr\n- [ ] Error message is shown if the output path is invalid/unwritable\n- [ ] Tests written and passing","status":"closed","priority":1,"issue_type":"bug","created_at":"2026-03-05T09:37:26.921804683Z","created_by":"oli","updated_at":"2026-03-05T12:10:00.000000000Z","closed_at":"2026-03-05T12:10:00.000000000Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0}
|
||||
{"id":"bd-bxt","title":"Add --version flag to CLI","description":"## Description\n\nAdd a `--version` flag to the `csv2excel` CLI that outputs version information including the version tag and build timestamp.\n\n## Expected Output\n\n```\ncsv2excel version v0.10.0-1-g3eb502a built 2026-02-19T14:30:13Z\n```\n\nThe version string should be injected at build time using Go linker flags (`-ldflags`), so that `git describe` and a build timestamp are embedded into the binary.\n\n## Implementation Notes\n\n- Define `version` and `buildDate` variables in `main.go` (or a dedicated `version.go`)\n- Inject values via `-ldflags \"-X main.version=... -X main.buildDate=...\"` during `mage Build`\n- Handle the `--version` / `-v` flag and print the version line, then exit 0\n\n## Acceptance Criteria\n- [ ] `csv2excel --version` prints a line matching `csv2excel version <git-describe> built <RFC3339-timestamp>`\n- [ ] Version and build date are injected at build time via ldflags in the Mage build targets\n- [ ] Running `csv2excel --version` exits with code 0\n- [ ] Default (unset) values produce a sensible fallback (e.g. `dev` / `unknown`)\n- [ ] Tests written and passing (if applicable)","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-03-05T09:22:40.538253362Z","created_by":"oli","updated_at":"2026-03-05T12:10:00.000000000Z","closed_at":"2026-03-05T12:10:00.000000000Z","close_reason":"done","source_repo":".","compaction_level":0,"original_size":0}
|
||||
4
.beads/metadata.json
Normal file
4
.beads/metadata.json
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"database": "beads.db",
|
||||
"jsonl_export": "issues.jsonl"
|
||||
}
|
||||
10
.idea/.gitignore
generated
vendored
Normal file
10
.idea/.gitignore
generated
vendored
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
9
.idea/csv2excel.iml
generated
Normal file
9
.idea/csv2excel.iml
generated
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="WEB_MODULE" version="4">
|
||||
<component name="Go" enabled="true" />
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
11
.idea/go.imports.xml
generated
Normal file
11
.idea/go.imports.xml
generated
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="GoImports">
|
||||
<option name="excludedPackages">
|
||||
<array>
|
||||
<option value="github.com/pkg/errors" />
|
||||
<option value="golang.org/x/net/context" />
|
||||
</array>
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
8
.idea/modules.xml
generated
Normal file
8
.idea/modules.xml
generated
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/csv2excel.iml" filepath="$PROJECT_DIR$/.idea/csv2excel.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
6
.idea/vcs.xml
generated
Normal file
6
.idea/vcs.xml
generated
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
73
AGENTS.md
Normal file
73
AGENTS.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# Agent Instructions
|
||||
|
||||
## Plans
|
||||
|
||||
- Make the plan extremely concise. Sacrifice grammar for the sake of concision.
|
||||
- At the end of each plan, give me a list of unresolved questions to answer, if any.
|
||||
|
||||
## Issue Tracking
|
||||
|
||||
This project uses **br** (beads_rust) for issue tracking and **bv** (beads_viewer) for triage/prioritization. Issues are stored in `.beads/` and tracked in git.
|
||||
|
||||
### Tool Roles
|
||||
|
||||
| Tool | Role | Key Commands |
|
||||
|------|------|--------------|
|
||||
| `br` | Issue management (CRUD) | `br ready`, `br show`, `br update`, `br close`, `br sync` |
|
||||
| `bv` | Triage & prioritization | `bv --robot-triage` |
|
||||
|
||||
### Quick Reference
|
||||
|
||||
```bash
|
||||
br ready # Find available work
|
||||
br show <id> # View issue details
|
||||
br update <id> --status in_progress # Claim work
|
||||
br close <id> # Complete work
|
||||
br sync # Sync with git
|
||||
```
|
||||
|
||||
### What to work on next?
|
||||
|
||||
```bash
|
||||
bv --robot-triage # AI-powered recommendation for next issue (preferred)
|
||||
br ready # Fallback: plain list of unblocked issues
|
||||
```
|
||||
|
||||
> **Warning:** bare `bv` (without flags) launches an interactive TUI that blocks the session. Always use `bv --robot-triage` or other non-interactive flags in automated contexts.
|
||||
|
||||
### Key Concepts
|
||||
|
||||
- **Dependencies**: Issues can block other issues. `br ready` shows only unblocked work.
|
||||
- **Priority**: P0=critical, P1=high, P2=medium, P3=low, P4=backlog (use numbers 0-4, not words)
|
||||
- **Types**: task, bug, feature, epic, chore, docs, question
|
||||
- **Blocking**: `br dep add <issue> <depends-on>` to add dependencies
|
||||
|
||||
|
||||
### Creating issues
|
||||
|
||||
You might be tasked with creating a new issue or you discover new tasks by yourself. Use the `/create-single-issue` command accordingly the create a new issue.
|
||||
|
||||
## Main Workflow
|
||||
|
||||
### Work on issue
|
||||
|
||||
You are tasked by the operator to work on an issue.
|
||||
It's either a specific issue (`/start-issue <id>`) or the *next* issue (`/start-next-issue`).
|
||||
The command tells you to open the issue, enter plan mode and then implement the plan.
|
||||
The command tells you explicitly *NOT* to: close the issue, commit or push anything (because this is subject to `/finish-issue`).
|
||||
|
||||
### Operator tests manually
|
||||
|
||||
After finishing the implementation the operator tests the solution.
|
||||
|
||||
### Finish issue
|
||||
|
||||
After testing you are tasked by the operator to finish the issue (`/finish-issue`).
|
||||
You close the issue and create a commit.
|
||||
|
||||
## Release Workflow
|
||||
|
||||
Once in a while the operator uses these commands:
|
||||
|
||||
- `/tag-version` - you create a new version using the `git tag` mechanism
|
||||
- `/update-changelog` - you update the CHANGELOG.md according to the changes of the last version
|
||||
31
CHANGELOG.md
Normal file
31
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Changelog
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.1.1] - 2026-03-05
|
||||
|
||||
### Fixed
|
||||
|
||||
- `-o` flag value was read as a pointer instead of a string, causing memory addresses to be printed and the output path to resolve to `.xlsx`
|
||||
|
||||
## [v0.1.0] - 2026-03-05
|
||||
|
||||
### Added
|
||||
|
||||
- Initial release: CLI tool to convert one or more CSV files into a single Excel (.xlsx) workbook
|
||||
- Each CSV file becomes a separate worksheet named after the source filename
|
||||
- Auto-detection of CSV delimiter (`,`, `;`, `\t`) based on first-line analysis
|
||||
- Support for UTF-8 and Windows-1252 encoding (`-enc` flag)
|
||||
- Configurable output file path (`-o` flag)
|
||||
- Tolerant CSV parsing with `LazyQuotes` for malformed files
|
||||
- Build system via [Mage](https://magefile.org/) with targets for Linux, Windows, and install
|
||||
- Version info injected at build time via git tags (`ldflags`)
|
||||
|
||||
### Documentation
|
||||
|
||||
- README with installation instructions, flag reference, and usage examples
|
||||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/magefile/mage/mg"
|
||||
"github.com/magefile/mage/sh"
|
||||
|
|
@ -23,12 +24,9 @@ func ldflags() (string, error) {
|
|||
if err != nil {
|
||||
commit = "none"
|
||||
}
|
||||
buildDate, err := sh.Output("date", "-u", "+%Y-%m-%dT%H:%M:%SZ")
|
||||
if err != nil {
|
||||
buildDate = "unknown"
|
||||
}
|
||||
buildDate := time.Now().UTC().Format(time.RFC3339)
|
||||
return fmt.Sprintf(
|
||||
`-X csv2excel/internal/version.Version=%s -X csv2excel/internal/version.Commit=%s -X csv2excel/internal/version.BuildDate=%s`,
|
||||
`-X code.beautifulmachines.dev/jakoubek/csv2excel/internal/version.Version=%s -X code.beautifulmachines.dev/jakoubek/csv2excel/internal/version.Commit=%s -X code.beautifulmachines.dev/jakoubek/csv2excel/internal/version.BuildDate=%s`,
|
||||
version, commit, buildDate,
|
||||
), nil
|
||||
}
|
||||
|
|
@ -77,6 +75,7 @@ func BuildWindows() error {
|
|||
|
||||
// Install installs the binary to $GOBIN or $GOPATH/bin.
|
||||
func Install() error {
|
||||
mg.Deps(Build)
|
||||
fmt.Println("Installing", binaryName, "...")
|
||||
flags, err := ldflags()
|
||||
if err != nil {
|
||||
|
|
|
|||
51
main.go
51
main.go
|
|
@ -12,27 +12,57 @@ import (
|
|||
"github.com/xuri/excelize/v2"
|
||||
"golang.org/x/text/encoding/charmap"
|
||||
"golang.org/x/text/transform"
|
||||
|
||||
"code.beautifulmachines.dev/jakoubek/csv2excel/internal/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
sep := flag.String("sep", "auto", "Trennzeichen: auto, ',', ';', '\\t'")
|
||||
enc := flag.String("enc", "utf8", "Encoding: utf8, windows1252")
|
||||
out := flag.String("o", "output.xlsx", "Ausgabedatei")
|
||||
showVersion := flag.Bool("version", false, "Version anzeigen")
|
||||
flag.Parse()
|
||||
|
||||
if *showVersion {
|
||||
fmt.Printf("csv2excel %s (commit %s, built %s)\n", version.Version, version.Commit, version.BuildDate)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// PowerShell kann "-o=testdatei.xlsx" in "-o=testdatei" + ".xlsx" splitten
|
||||
if filepath.Ext(*out) == "" {
|
||||
*out += ".xlsx"
|
||||
}
|
||||
|
||||
files := flag.Args()
|
||||
if len(files) == 0 {
|
||||
fmt.Fprintln(os.Stderr, "Verwendung: csv2xlsx [flags] datei1.csv datei2.csv ...")
|
||||
fmt.Fprintln(os.Stderr, "Verwendung: csv2excel [flags] datei1.csv datei2.csv ...")
|
||||
fmt.Fprintln(os.Stderr, "Tipp: In PowerShell Leerzeichen statt = verwenden: -o ausgabe.xlsx")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println(sep)
|
||||
fmt.Println(enc)
|
||||
fmt.Println(out)
|
||||
if err := run(files, *out, *sep, *enc); err != nil {
|
||||
fmt.Fprintln(os.Stderr, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func run(files []string, out, sep, enc string) error {
|
||||
xlsx := excelize.NewFile()
|
||||
firstSheet := true
|
||||
|
||||
var csvFiles []string
|
||||
for _, f := range files {
|
||||
if strings.EqualFold(filepath.Ext(f), ".xlsx") {
|
||||
fmt.Fprintf(os.Stderr, "Warnung: %s übersprungen – Excel-Datei als Input? Meintest du: -o=%s\n", f, f)
|
||||
continue
|
||||
}
|
||||
csvFiles = append(csvFiles, f)
|
||||
}
|
||||
files = csvFiles
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("keine CSV-Dateien zum Verarbeiten")
|
||||
}
|
||||
|
||||
for _, path := range files {
|
||||
|
||||
sheetName := strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))
|
||||
|
|
@ -49,11 +79,11 @@ func main() {
|
|||
defer f.Close()
|
||||
|
||||
var reader io.Reader = f
|
||||
if *enc == "windows1252" {
|
||||
if enc == "windows1252" {
|
||||
reader = transform.NewReader(f, charmap.Windows1252.NewDecoder())
|
||||
}
|
||||
|
||||
delimiter := detectDelimiter(path, *sep)
|
||||
delimiter := detectDelimiter(path, sep)
|
||||
|
||||
csvReader := csv.NewReader(reader)
|
||||
csvReader.Comma = delimiter
|
||||
|
|
@ -85,12 +115,11 @@ func main() {
|
|||
fmt.Printf("✓ %s → Reiter \"%s\" (%d Zeilen)\n", path, sheetName, row-1)
|
||||
}
|
||||
|
||||
if err := xlsx.SaveAs(*out); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Fehler beim Speichern: %v\n", err)
|
||||
os.Exit(1)
|
||||
if err := xlsx.SaveAs(out); err != nil {
|
||||
return fmt.Errorf("Fehler beim Speichern: %w", err)
|
||||
}
|
||||
fmt.Printf("✅ Gespeichert: %s\n", *out)
|
||||
|
||||
fmt.Printf("✅ Gespeichert: %s\n", out)
|
||||
return nil
|
||||
}
|
||||
|
||||
func detectDelimiter(path, hint string) rune {
|
||||
|
|
|
|||
106
main_test.go
Normal file
106
main_test.go
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/xuri/excelize/v2"
|
||||
)
|
||||
|
||||
func writeTempCSV(t *testing.T, dir, name, content string) string {
|
||||
t.Helper()
|
||||
path := filepath.Join(dir, name)
|
||||
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
|
||||
t.Fatalf("WriteFile: %v", err)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
func TestRun_OutputPath(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
csv := writeTempCSV(t, dir, "data.csv", "name,age\nAlice,30\nBob,25\n")
|
||||
out := filepath.Join(dir, "result.xlsx")
|
||||
|
||||
if err := run([]string{csv}, out, "auto", "utf8"); err != nil {
|
||||
t.Fatalf("run() error: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(out); os.IsNotExist(err) {
|
||||
t.Fatalf("output file not created: %s", out)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_SheetContent(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
csv := writeTempCSV(t, dir, "sheet1.csv", "col1;col2\nfoo;bar\n")
|
||||
out := filepath.Join(dir, "out.xlsx")
|
||||
|
||||
if err := run([]string{csv}, out, ";", "utf8"); err != nil {
|
||||
t.Fatalf("run() error: %v", err)
|
||||
}
|
||||
|
||||
f, err := excelize.OpenFile(out)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenFile: %v", err)
|
||||
}
|
||||
|
||||
val, err := f.GetCellValue("sheet1", "A1")
|
||||
if err != nil {
|
||||
t.Fatalf("GetCellValue: %v", err)
|
||||
}
|
||||
if val != "col1" {
|
||||
t.Errorf("A1 = %q, want %q", val, "col1")
|
||||
}
|
||||
|
||||
val, err = f.GetCellValue("sheet1", "B2")
|
||||
if err != nil {
|
||||
t.Fatalf("GetCellValue: %v", err)
|
||||
}
|
||||
if val != "bar" {
|
||||
t.Errorf("B2 = %q, want %q", val, "bar")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_MultipleSheets(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
csv1 := writeTempCSV(t, dir, "alpha.csv", "x\n1\n")
|
||||
csv2 := writeTempCSV(t, dir, "beta.csv", "y\n2\n")
|
||||
out := filepath.Join(dir, "multi.xlsx")
|
||||
|
||||
if err := run([]string{csv1, csv2}, out, "auto", "utf8"); err != nil {
|
||||
t.Fatalf("run() error: %v", err)
|
||||
}
|
||||
|
||||
f, err := excelize.OpenFile(out)
|
||||
if err != nil {
|
||||
t.Fatalf("OpenFile: %v", err)
|
||||
}
|
||||
|
||||
sheets := f.GetSheetList()
|
||||
if len(sheets) != 2 {
|
||||
t.Fatalf("got %d sheets, want 2", len(sheets))
|
||||
}
|
||||
if sheets[0] != "alpha" || sheets[1] != "beta" {
|
||||
t.Errorf("sheets = %v, want [alpha beta]", sheets)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRun_CustomOutputFilename(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
csv := writeTempCSV(t, dir, "input.csv", "a,b\n1,2\n")
|
||||
out := filepath.Join(dir, "custom_name.xlsx")
|
||||
|
||||
if err := run([]string{csv}, out, ",", "utf8"); err != nil {
|
||||
t.Fatalf("run() error: %v", err)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(out); os.IsNotExist(err) {
|
||||
t.Fatalf("expected output at %s, not found", out)
|
||||
}
|
||||
// ensure default name was NOT created
|
||||
defaultOut := filepath.Join(dir, "output.xlsx")
|
||||
if _, err := os.Stat(defaultOut); !os.IsNotExist(err) {
|
||||
t.Errorf("unexpected file created at %s", defaultOut)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue