Compare commits

..

No commits in common. '192b41e18be75cfb2da475349ff16f8a80792d0e' and '2bead6af27edd611c9d2c212966a9ac4a73b8b1b' have entirely different histories.

  1. 2
      .gitignore
  2. 2
      .opencode/commands/commit.md
  3. 24
      .opencode/skills/make-commit/SKILL.md
  4. 190
      README.md
  5. 13
      cmd/declpac/main.go
  6. 2
      openspec/changes/add-operation-logging/.openspec.yaml
  7. 66
      openspec/changes/add-operation-logging/design.md
  8. 26
      openspec/changes/add-operation-logging/proposal.md
  9. 45
      openspec/changes/add-operation-logging/tasks.md
  10. 59
      pkg/fetch/fetch.go
  11. 56
      pkg/pacman/pacman.go
  12. 43
      pkg/state/state.go

2
.gitignore vendored

@ -32,4 +32,4 @@ go.work.sum
# .vscode/ # .vscode/
# Binary output # Binary output
/declpac declpac

2
.opencode/commands/commit.md

@ -2,4 +2,4 @@
description: Make a git commit, asking if it was by the user or AI description: Make a git commit, asking if it was by the user or AI
--- ---
skill [name=make-commit] message: $1 skill [name=make-commit] $1

24
.opencode/skills/make-commit/SKILL.md

@ -16,7 +16,7 @@ Make a git commit, distinguishing between user and AI contributions.
**Steps** **Steps**
1. **[REQUIRED] Ask user if this commit is by them or by AI** 1. **Ask user if this commit is by them or by AI**
Use the **question tool** to ask: Use the **question tool** to ask:
> "Was this commit made by you or by AI?" > "Was this commit made by you or by AI?"
@ -27,23 +27,22 @@ Make a git commit, distinguishing between user and AI contributions.
2. **Check for commit message** 2. **Check for commit message**
**Capitalization rule**: Commit messages should start with a capital letter,
unless it refers to a tool or project that explicitly uses lowercase as its
name (e.g., "go", "npm", "rustc").
If the user did NOT provide a commit message, generate one from staged changes: If the user did NOT provide a commit message, generate one from staged changes:
```bash ```bash
git diff --staged --stat git diff --staged --stat
``` ```
Create a reasonable commit message based on the changes. Create a reasonable commit message based on the changes.
If thed user DID provide a message, format it into a proper commit message. **Capitalization rule**: Commit message should start with a capital letter,
unless it refers to a tool or project that explicitly uses lowercase as its
name (e.g., "go", "npm", "rustc").
3. **Show commit message and confirm** 3. **Show commit message and confirm**
Display the commit message to the user.
Use the **question tool** to ask: Use the **question tool** to ask:
> "Is this commit message okay, or would you like to make tweaks?" > "Is this commit message okay, or would you like to make tweaks?"
> <message>
Options: Options:
- "Looks good" - Proceed with this message - "Looks good" - Proceed with this message
@ -51,7 +50,14 @@ Make a git commit, distinguishing between user and AI contributions.
**If user wants tweaks**: Ask them for the new commit message. **If user wants tweaks**: Ask them for the new commit message.
4. **Make the commit** 4. **Get git user config**
```bash
git config user.name
git config user.email
```
5. **Make the commit**
Use the commit message provided by the user. Use the commit message provided by the user.
@ -63,7 +69,7 @@ Make a git commit, distinguishing between user and AI contributions.
**If by AI:** **If by AI:**
```bash ```bash
git commit -m "<message>" --author="AI Bot <ai@local>" git -c user.name="<git-config-name>" -c user.email="<git-config-email>" commit -m "<message>" --author="AI Bot <ai@local>"
``` ```
(Uses git config for committer, but sets author to AI Bot) (Uses git config for committer, but sets author to AI Bot)

190
README.md

@ -1,14 +1,18 @@
# declpac # declpac
Declarative package manager for Arch Linux that syncs your system with a declared package list using pacman. `declpac` is a declarative package manager for Arch Linux that syncs your system
with a declared package list using `pacman`. It ensures your system matches your
desired state, handling package installation, upgrades, and orphan cleanup
automatically.
## Features ## Features
- Declarative state management — define your desired package list in files or stdin - **Declarative state management** — Define your desired package list in files or stdin
- Smart orphan cleanup — removes packages no longer needed - **Automatic dependency resolution** — Pacman handles transitive dependencies
- Explicit package tracking — marks declared packages as explicit - **Smart orphan cleanup** — Removes packages no longer needed
- AUR support — builds and installs AUR packages automatically - **Explicit package tracking** — Marks your declared packages as explicit
- Machine-readable output — perfect for scripting - **AUR support** — Falls back to AUR for packages not in official repos
- **Machine-readable output** — Perfect for scripting and automation
## Installation ## Installation
@ -24,13 +28,14 @@ sudo mv declpac /usr/local/bin/
### Dependencies ### Dependencies
- Go 1.21+ - Go 1.21+
- pacman - pacman (system package manager)
- makepkg (for AUR support) - aur (AUR helper, optional for AUR support)
- git (for AUR support) - Root privileges (required for pacman operations)
- Root privileges
## Usage ## Usage
### Basic Usage
```bash ```bash
# Single state file # Single state file
sudo declpac --state packages.txt sudo declpac --state packages.txt
@ -40,63 +45,119 @@ sudo declpac --state base.txt --state apps.txt
# From stdin # From stdin
cat packages.txt | sudo declpac cat packages.txt | sudo declpac
# Preview changes without applying
sudo declpac --dry-run --state packages.txt
``` ```
### State File Format ### State File Format
One package name per line, lines beginning with `#` are comments: State files contain one package name per line:
``` ```
bash bash
vim vim
git git
docker docker
# this is a comment
``` ```
### Options Lines are treated as package names with whitespace trimmed:
```
bash # bash
vim # vim
# comment # ignored
```
### Command Line Options
| Flag | Alias | Description | | Flag | Alias | Description |
|------|-------|-------------| |------|-------|-------------|
| `--state` | `-s` | State file to read package list from (can be used multiple times) | | `--state` | `-s` | State file(s) to read package list from (can be used multiple times) |
| `--dry-run` | | Preview changes without applying them | | `--yes` | `-y` | Skip confirmation prompts (for scripting) |
| `--dry-run` | | Simulate sync without making changes |
| `--help` | `-h` | Show help message | | `--help` | `-h` | Show help message |
### Examples
#### Minimal System
```bash
# Create a minimal system package list
echo -e "base\nbase-devel\nlinux-headers\nvim\ngit\ncurl\nwget" > ~/.config/declpac/minimal.txt
# Apply the state
sudo declpac --state ~/.config/declpac/minimal.txt
```
#### Development Environment
```bash
# development.txt
go
nodejs
python
rust
docker
docker-compose
kubectl
helm
terraform
# Apply
sudo declpac --state development.txt
```
#### Full System Sync
```bash
# Combine multiple files
sudo declpac --state ~/.config/declpac/base.txt --state ~/.config/declpac/desktop.txt
# Or use stdin
cat ~/.config/declpac/full-system.txt | sudo declpac
```
#### Dry-Run Preview
```bash
# Preview what would happen without making changes
sudo declpac --dry-run --state packages.txt
# Example output:
# Installed 3 packages, removed 2 packages
# Would install: vim, git, docker
# Would remove: python2, perl-xml-parser
```
## How It Works ## How It Works
1. **Read** — Collect packages from all state files and stdin 1. **Collect packages** — Reads from all `--state` files and stdin
2. **Merge** — Combine into single package list 2. **Merge** — Combines all packages additively (duplicates allowed)
3. **Categorize** — Check if packages are in official repos or AUR 3. **Validate** — Checks packages exist in repos or AUR
4. **Sync** — Install/update packages via pacman 4. **Mark explicit** — Marks declared packages as explicit dependencies
5. **Build** — Build and install AUR packages via makepkg 5. **Sync** — Runs `pacman -Syu` to install/upgrade packages
6. **Mark** — Mark declared packages as explicit, all others as dependencies 6. **Cleanup** — Removes orphaned packages with `pacman -Rns`
7. **Cleanup** — Remove orphaned packages 7. **Report** — Outputs summary: `Installed X packages, removed Y packages`
### Database Freshness ### Database Freshness
If the pacman database is older than 24 hours, it is automatically refreshed. If the pacman database is older than 24 hours, `declpac` automatically refreshes it with `pacman -Syy` before validation.
### Orphan Cleanup
### Logging After syncing, `declpac` identifies and removes packages that are:
- Not explicitly installed
- Not required by any other package
Operations are logged to `/var/log/declpac.log`. This keeps your system clean from dependency artifacts.
## Output ## Output Format
``` ```
# Packages installed/removed # Success (packages installed/removed)
Installed 5 packages, removed 2 packages Installed 5 packages, removed 2 packages
# No changes needed # Success (no changes)
Installed 0 packages, removed 0 packages Installed 0 packages, removed 0 packages
# Dry-run preview
Installed 3 packages, removed 1 packages
Would install: vim, git, docker
Would remove: python2
# Error # Error
error: package not found: <package-name> error: package not found: <package-name>
``` ```
@ -106,54 +167,36 @@ error: package not found: <package-name>
| Code | Meaning | | Code | Meaning |
|------|---------| |------|---------|
| 0 | Success | | 0 | Success |
| 1 | Error | | 1 | Error (no packages, validation failure, pacman error) |
## Examples
### Minimal System ## Security Considerations
```bash - **Run as root**`declpac` requires root privileges for pacman operations
echo -e "base\nbase-devel\nlinux-headers\nvim\ngit\ncurl" > ~/.config/declpac/minimal.txt - **Review state files** — Only install packages from trusted sources
sudo declpac --state ~/.config/declpac/minimal.txt - **Backup** — Consider backing up your system before major changes
```
### Development Environment
```bash
# development.txt
go
nodejs
python
rust
docker
sudo declpac --state development.txt
```
### Dry-Run
```bash
sudo declpac --dry-run --state packages.txt
```
## Troubleshooting ## Troubleshooting
### Permission denied ### "Permission denied"
`declpac` requires root privileges. Use `sudo`:
Use sudo:
```bash ```bash
sudo declpac --state packages.txt sudo declpac --state packages.txt
``` ```
### Package not found ### "Package not found"
The package doesn't exist in pacman repos or AUR. Check the package name:
Check if the package exists:
```bash ```bash
pacman -Ss <package> pacman -Ss <package>
``` ```
### Database sync fails ### Database sync fails
Refresh manually:
```bash ```bash
sudo pacman -Syy sudo pacman -Syy
``` ```
@ -167,14 +210,13 @@ declpac/
├── pkg/ ├── pkg/
│ ├── input/ # State file/stdin reading │ ├── input/ # State file/stdin reading
│ ├── merge/ # Package merging │ ├── merge/ # Package merging
│ ├── fetch/ # Package resolution (pacman/AUR) │ ├── validation/ # Package validation
│ ├── pacman/ # Pacman operations │ ├── pacman/ # Pacman integration
│ ├── validation/ # Database freshness check │ └── output/ # Output formatting
│ ├── output/ # Output formatting ├── go.mod # Go module
│ └── state/ # Logging └── README.md # This file
└── README.md
``` ```
## License ## License
GPL-3.0 GPL-3.0

13
cmd/declpac/main.go

@ -12,7 +12,6 @@ import (
"github.com/Riyyi/declpac/pkg/merge" "github.com/Riyyi/declpac/pkg/merge"
"github.com/Riyyi/declpac/pkg/output" "github.com/Riyyi/declpac/pkg/output"
"github.com/Riyyi/declpac/pkg/pacman" "github.com/Riyyi/declpac/pkg/pacman"
"github.com/Riyyi/declpac/pkg/state"
"github.com/Riyyi/declpac/pkg/validation" "github.com/Riyyi/declpac/pkg/validation"
) )
@ -35,6 +34,12 @@ func main() {
Usage: "State file(s) to read package list from", Usage: "State file(s) to read package list from",
Destination: &cfg.StateFiles, Destination: &cfg.StateFiles,
}, },
&cli.BoolFlag{
Name: "yes",
Aliases: []string{"y"},
Usage: "Skip confirmation prompts",
Destination: &cfg.NoConfirm,
},
&cli.BoolFlag{ &cli.BoolFlag{
Name: "dry-run", Name: "dry-run",
Usage: "Simulate the sync without making changes", Usage: "Simulate the sync without making changes",
@ -52,12 +57,6 @@ func main() {
} }
func run(cfg *Config) error { func run(cfg *Config) error {
if err := state.OpenLog(); err != nil {
fmt.Fprintf(os.Stderr, "error: %v\n", err)
return err
}
defer state.Close()
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] run: starting...\n") fmt.Fprintf(os.Stderr, "[debug] run: starting...\n")

2
openspec/changes/add-operation-logging/.openspec.yaml

@ -1,2 +0,0 @@
schema: spec-driven
created: 2026-04-15

66
openspec/changes/add-operation-logging/design.md

@ -1,66 +0,0 @@
## Implementation
### Log File Location
- Path: `/var/log/declpac.log`
- Single merged log file (stdout + stderr intermingled in order of arrival)
### State-Modifying Functions (need logging)
1. `SyncPackages()` - `pacman -S --needed <packages>`
2. `InstallAUR()` - `git clone` + `makepkg -si --noconfirm`
3. `MarkAllAsDeps()` - `pacman -D --asdeps`
4. `MarkAsExplicit()` - `pacman -D --asexplicit <packages>`
5. `CleanupOrphans()` - `pacman -Rns`
### Functions to Skip (read-only)
- `DryRun()` - queries only
- `getInstalledCount()` - pacman -Qq
### Execution Patterns
| Pattern | Functions | How |
|---------|------------|-----|
| Streaming | MarkAllAsDeps, MarkAsExplicit, InstallAUR | `io.MultiWriter` to tee to both terminal and log |
| Captured | SyncPackages, CleanupOrphans | capture with `CombinedOutput()`, write to log, write to terminal |
### Error Handling
- Write error to log BEFORE returning from function
- Print error to stderr so user sees it
### Dependencies
- Add to imports: `io`, `os`, `path/filepath`
### Structure
```go
// pkg/state/state.go
var logFile *os.File
func OpenLog() error {
logPath := filepath.Join("/var/log", "declpac.log")
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
logFile = f
return nil
}
func GetLogWriter() io.Writer {
return logFile
}
func Close() error {
if logFile == nil {
return nil
}
return logFile.Close()
}
```
### Flow in Sync() or main entrypoint
1. Call `OpenLog()` at program start, defer close
2. Each state-modifying function uses `state.GetLogWriter()` via MultiWriter
### Wire into main.go
- Open log at start of `run()`
- Pass log writer to pacman package (via exported function or global)

26
openspec/changes/add-operation-logging/proposal.md

@ -1,26 +0,0 @@
## Why
The tool exits without saving the full pacman output, making debugging difficult
when operations fail. Users need a persistent log of all pacman operations for
debugging and audit.
## What Changes
- Add state directory initialization creating `~/.local/state/declpac` if not exists
- Open/manage a single log file at `$XDG_STATE_HOME/declpac` (e.g., `~/.local/state/declpac/declpac`)
- Instrument all state-modifying exec calls in `pkg/pacman/pacman.go` to tee or append output to this file
- Skip debug messages (internal timing logs)
- Capture and write errors before returning
## Capabilities
### New Capabilities
- Operation logging: Persist stdout/stderr from all pacman operations
### Modified Capabilities
- None
## Impact
- `pkg/pacman/pacman.go`: Instrument all state-modifying functions to write to log file
- New module: May create `pkg/state/state.go` or similar for log file management

45
openspec/changes/add-operation-logging/tasks.md

@ -1,45 +0,0 @@
## Tasks
- [x] 1. Create state module
Create `pkg/state/state.go`:
- `OpenLog()` - opens `/var/log/declpac.log` in append mode
- `GetLogWriter()` - returns the raw log file writer (for MultiWriter)
- `Write(msg []byte)` - writes message with timestamp + dashes separator
- `Close()` - closes the file
- [x] 2. Wire into main.go
In `cmd/declpac/main.go` `run()`:
- Call `OpenLog()` at start
- `defer Close()` log
- [x] 3. Instrument pkg/pacman
Modify `pkg/pacman/pacman.go`:
All state-modifying functions use `state.Write()` instead of `state.GetLogWriter().Write()`:
```
// OLD
state.GetLogWriter().Write(output)
// NEW
state.Write(output) // auto-prepends timestamp
```
**Functions updated:**
- `SyncPackages()` - write output with timestamp
- `CleanupOrphans()` - write output with timestamp
- `MarkAllAsDeps()` - write operation name with timestamp before running
- `MarkAsExplicit()` - write operation name with timestamp before running
- `InstallAUR()` - write "Cloning ..." and "Building package..." with timestamps
- Error handling - `state.Write([]byte("error: ..."))` for all error paths
- [x] 4. Add io import
Add `io` to imports in pacman.go
- [x] 5. Test
Run a sync operation and verify log file created at `/var/log/declpac.log`

59
pkg/fetch/fetch.go

@ -188,9 +188,6 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
fmt.Fprintf(os.Stderr, "[debug] Resolve: starting...\n") fmt.Fprintf(os.Stderr, "[debug] Resolve: starting...\n")
result := make(map[string]*PackageInfo) result := make(map[string]*PackageInfo)
for _, pkg := range packages {
result[pkg] = &PackageInfo{Name: pkg, Exists: true}
}
localPkgs, err := f.buildLocalPkgMap() localPkgs, err := f.buildLocalPkgMap()
if err != nil { if err != nil {
@ -218,38 +215,44 @@ func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
fmt.Fprintf(os.Stderr, "[debug] Resolve: sync db checked (%.2fs)\n", time.Since(start).Seconds())
f.ensureAURCache(packages) var notInSync []string
for _, pkg := range notInLocal {
for _, pkg := range packages { if syncPkg, ok := syncPkgs[pkg]; ok {
info := result[pkg] result[pkg] = &PackageInfo{
if info == nil { Name: pkg,
continue Exists: true,
InAUR: false,
Installed: false,
syncPkg: syncPkg,
}
} else {
notInSync = append(notInSync, pkg)
} }
}
if info.Installed { if len(notInSync) > 0 {
f.ensureAURCache(notInSync)
fmt.Fprintf(os.Stderr, "[debug] Resolve: AUR cache ensured (%.2fs)\n", time.Since(start).Seconds())
var unfound []string
for _, pkg := range notInSync {
if aurInfo, ok := f.aurCache[pkg]; ok { if aurInfo, ok := f.aurCache[pkg]; ok {
info.InAUR = true result[pkg] = &PackageInfo{
info.AURInfo = &aurInfo Name: pkg,
Exists: true,
InAUR: true,
Installed: false,
AURInfo: &aurInfo,
}
} else {
unfound = append(unfound, pkg)
} }
continue
}
if syncPkg, ok := syncPkgs[pkg]; ok {
info.InAUR = false
info.Installed = false
info.syncPkg = syncPkg
continue
} }
if len(unfound) > 0 {
if aurInfo, ok := f.aurCache[pkg]; ok { return nil, fmt.Errorf("package(s) not found: %s", strings.Join(unfound, ", "))
info.InAUR = true
info.Installed = false
info.AURInfo = &aurInfo
continue
} }
return nil, fmt.Errorf("package not found: %s", pkg)
} }
} }

56
pkg/pacman/pacman.go

@ -2,7 +2,6 @@ package pacman
import ( import (
"fmt" "fmt"
"io"
"os" "os"
"os/exec" "os/exec"
"regexp" "regexp"
@ -11,7 +10,6 @@ import (
"github.com/Riyyi/declpac/pkg/fetch" "github.com/Riyyi/declpac/pkg/fetch"
"github.com/Riyyi/declpac/pkg/output" "github.com/Riyyi/declpac/pkg/output"
"github.com/Riyyi/declpac/pkg/state"
"github.com/Riyyi/declpac/pkg/validation" "github.com/Riyyi/declpac/pkg/validation"
) )
@ -20,13 +18,9 @@ func MarkAllAsDeps() error {
fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: starting...\n") fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: starting...\n")
cmd := exec.Command("pacman", "-D", "--asdeps") cmd := exec.Command("pacman", "-D", "--asdeps")
state.Write([]byte("MarkAllAsDeps...\n")) cmd.Stdout = os.Stdout
cmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter()) cmd.Stderr = os.Stderr
cmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter())
err := cmd.Run() err := cmd.Run()
if err != nil {
state.Write([]byte(fmt.Sprintf("error: %v\n", err)))
}
fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: done (%.2fs)\n", time.Since(start).Seconds()) fmt.Fprintf(os.Stderr, "[debug] MarkAllAsDeps: done (%.2fs)\n", time.Since(start).Seconds())
return err return err
@ -41,13 +35,9 @@ func MarkAsExplicit(packages []string) error {
args := append([]string{"-D", "--asexplicit"}, packages...) args := append([]string{"-D", "--asexplicit"}, packages...)
cmd := exec.Command("pacman", args...) cmd := exec.Command("pacman", args...)
state.Write([]byte("MarkAsExplicit...\n")) cmd.Stdout = os.Stdout
cmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter()) cmd.Stderr = os.Stderr
cmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter())
err := cmd.Run() err := cmd.Run()
if err != nil {
state.Write([]byte(fmt.Sprintf("error: %v\n", err)))
}
fmt.Fprintf(os.Stderr, "[debug] MarkAsExplicit: done (%.2fs)\n", time.Since(start).Seconds()) fmt.Fprintf(os.Stderr, "[debug] MarkAsExplicit: done (%.2fs)\n", time.Since(start).Seconds())
return err return err
@ -167,25 +157,19 @@ func InstallAUR(f *fetch.Fetcher, pkgName string) error {
defer os.RemoveAll(tmpDir) defer os.RemoveAll(tmpDir)
cloneURL := "https://aur.archlinux.org/" + aurInfo.PackageBase + ".git" cloneURL := "https://aur.archlinux.org/" + aurInfo.PackageBase + ".git"
state.Write([]byte("Cloning " + cloneURL + "\n"))
cloneCmd := exec.Command("git", "clone", cloneURL, tmpDir) cloneCmd := exec.Command("git", "clone", cloneURL, tmpDir)
cloneCmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter()) cloneCmd.Stdout = os.Stdout
cloneCmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter()) cloneCmd.Stderr = os.Stderr
if err := cloneCmd.Run(); err != nil { if err := cloneCmd.Run(); err != nil {
errMsg := fmt.Sprintf("failed to clone AUR repo: %w\n", err)
state.Write([]byte("error: " + errMsg))
return fmt.Errorf("failed to clone AUR repo: %w", err) return fmt.Errorf("failed to clone AUR repo: %w", err)
} }
fmt.Fprintf(os.Stderr, "[debug] InstallAUR: cloned (%.2fs)\n", time.Since(start).Seconds()) fmt.Fprintf(os.Stderr, "[debug] InstallAUR: cloned (%.2fs)\n", time.Since(start).Seconds())
state.Write([]byte("Building package...\n"))
makepkgCmd := exec.Command("makepkg", "-si", "--noconfirm") makepkgCmd := exec.Command("makepkg", "-si", "--noconfirm")
makepkgCmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter()) makepkgCmd.Stdout = os.Stdout
makepkgCmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter()) makepkgCmd.Stderr = os.Stderr
makepkgCmd.Dir = tmpDir makepkgCmd.Dir = tmpDir
if err := makepkgCmd.Run(); err != nil { if err := makepkgCmd.Run(); err != nil {
errMsg := fmt.Sprintf("makepkg failed to build AUR package: %w\n", err)
state.Write([]byte("error: " + errMsg))
return fmt.Errorf("makepkg failed to build AUR package: %w", err) return fmt.Errorf("makepkg failed to build AUR package: %w", err)
} }
fmt.Fprintf(os.Stderr, "[debug] InstallAUR: built (%.2fs)\n", time.Since(start).Seconds()) fmt.Fprintf(os.Stderr, "[debug] InstallAUR: built (%.2fs)\n", time.Since(start).Seconds())
@ -216,19 +200,13 @@ func SyncPackages(packages []string) (int, error) {
start := time.Now() start := time.Now()
fmt.Fprintf(os.Stderr, "[debug] SyncPackages: starting...\n") fmt.Fprintf(os.Stderr, "[debug] SyncPackages: starting...\n")
args := append([]string{"-S", "--needed"}, packages...) args := append([]string{"-Syu"}, packages...)
cmd := exec.Command("pacman", args...) cmd := exec.Command("pacman", args...)
output, err := cmd.CombinedOutput() output, err := cmd.CombinedOutput()
if err != nil { if err != nil {
errMsg := fmt.Sprintf("pacman sync failed: %s", output)
state.Write([]byte(errMsg))
return 0, fmt.Errorf("pacman sync failed: %s", output) return 0, fmt.Errorf("pacman sync failed: %s", output)
} }
if len(output) > 0 {
state.Write(output)
}
re := regexp.MustCompile(`upgrading (\S+)`) re := regexp.MustCompile(`upgrading (\S+)`)
matches := re.FindAllStringSubmatch(string(output), -1) matches := re.FindAllStringSubmatch(string(output), -1)
@ -255,15 +233,9 @@ func CleanupOrphans() (int, error) {
removeCmd := exec.Command("pacman", "-Rns") removeCmd := exec.Command("pacman", "-Rns")
output, err := removeCmd.CombinedOutput() output, err := removeCmd.CombinedOutput()
if err != nil { if err != nil {
errMsg := fmt.Sprintf("%s: %s", err, output)
state.Write([]byte(errMsg))
return 0, fmt.Errorf("%s: %s", err, output) return 0, fmt.Errorf("%s: %s", err, output)
} }
if len(output) > 0 {
state.Write(output)
}
count := len(orphans) count := len(orphans)
fmt.Fprintf(os.Stderr, "[debug] CleanupOrphans: done (%.2fs)\n", time.Since(start).Seconds()) fmt.Fprintf(os.Stderr, "[debug] CleanupOrphans: done (%.2fs)\n", time.Since(start).Seconds())
@ -299,10 +271,12 @@ func DryRun(packages []string) (*output.Result, error) {
if info == nil || !info.Exists { if info == nil || !info.Exists {
return nil, fmt.Errorf("package not found: %s", pkg) return nil, fmt.Errorf("package not found: %s", pkg)
} }
if info.InAUR { if _, installed := localPkgs[pkg]; !installed {
aurPkgs = append(aurPkgs, pkg) if info.InAUR {
} else if _, installed := localPkgs[pkg]; !installed { aurPkgs = append(aurPkgs, pkg)
toInstall = append(toInstall, pkg) } else {
toInstall = append(toInstall, pkg)
}
} }
} }
fmt.Fprintf(os.Stderr, "[debug] DryRun: packages categorized (%.2fs)\n", time.Since(start).Seconds()) fmt.Fprintf(os.Stderr, "[debug] DryRun: packages categorized (%.2fs)\n", time.Since(start).Seconds())

43
pkg/state/state.go

@ -1,43 +0,0 @@
package state
import (
"fmt"
"io"
"os"
"path/filepath"
"time"
)
var logFile *os.File
func OpenLog() error {
logPath := filepath.Join("/var/log", "declpac.log")
f, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
return err
}
logFile = f
return nil
}
func GetLogWriter() io.Writer {
return logFile
}
func Write(msg []byte) {
PrependWithTimestamp(logFile, msg)
}
func Close() error {
if logFile == nil {
return nil
}
return logFile.Close()
}
func PrependWithTimestamp(w io.Writer, msg []byte) {
ts := time.Now().Format("2006-01-02 15:04:05")
header := fmt.Sprintf("\n--- %s ---\n", ts)
w.Write([]byte(header))
w.Write(msg)
}
Loading…
Cancel
Save