Archive experimental OpenSpec changes

This commit is contained in:
AI Bot
2026-04-17 19:35:25 +02:00
committed by Riyyi
parent b4b12b39b6
commit 01338cb671
39 changed files with 0 additions and 0 deletions
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-15
@@ -0,0 +1,66 @@
## 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)
@@ -0,0 +1,26 @@
## 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
@@ -0,0 +1,45 @@
## 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`
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-14
@@ -0,0 +1,137 @@
## Context
Currently, `pkg/pacman/pacman.go` uses subprocess calls to query pacman for package existence:
- `pacman -Qip <pkg>` to check local DB (per package)
- `pacman -Sip <pkg>` to check sync repos (per package)
For n packages, this spawns 2n subprocesses (up to ~300 for typical package lists). Each subprocess has fork/exec overhead, making this the primary performance bottleneck.
The AUR queries are already batched (single HTTP POST with all package names), which is the desired pattern.
## Goals / Non-Goals
**Goals:**
- Eliminate subprocess overhead for local/sync DB package lookups
- Maintain batched AUR HTTP calls (single request per batch)
- Track installed status per package in PackageInfo
- Provide dry-run output showing exact packages to install/remove
- Handle orphan cleanup correctly
**Non-Goals:**
- Parallel AUR builds (still sequential)
- Custom pacman transaction handling (use system pacman)
- Repository configuration changes
- Package download/compile optimization
## Decisions
### 1. Use Jguer/dyalpm for DB access
**Decision**: Use `github.com/Jguer/dyalpm` library instead of spawning subprocesses.
**Rationale**:
- Direct libalpm access (same backend as pacman)
- Already Go-native with proper type safety
- Supports batch operations via `GetPkgCache()` and `PkgCache()` iterators
**Alternatives considered**:
- Parse `pacman -Qs` output - fragile, still subprocess-based
- Write custom libalpm bindings - unnecessary effort
### 2. Single-pass package resolution algorithm
**Decision**: Process all packages through local DB → sync DBs → AUR in one pass.
```
For each package in collected state:
1. Check local DB (batch lookup) → if found, mark Installed=true
2. If not local, check all sync DBs (batch lookup per repo)
3. If not in sync, append to AUR batch
Batch query AUR with all remaining packages
Throw error if any package not found in local/sync/AUR
Collect installed status from local DB
(Perform sync operations - skip in dry-run)
(Mark ALL currently installed packages as deps - skip in dry-run)
(Then mark collected state packages as explicit - skip in dry-run)
(Cleanup orphans - skip in dry-run)
Output summary
```
**Rationale**:
- Single iteration over packages
- Batch DB lookups minimize libalpm calls
- Clear error handling for missing packages
- Consistent with existing behavior
### 3. Batch local/sync DB lookup implementation
**Decision**: For local DB, iterate `localDB.PkgCache()` once and build a map. For sync DBs, iterate each repo's `PkgCache()`.
**Implementation**:
```go
// Build local package map in one pass
localPkgs := make(map[string]bool)
localDB.PkgCache().ForEach(func(pkg alpm.Package) error {
localPkgs[pkg.Name()] = true
return nil
})
// Similarly for each sync DB
for _, syncDB := range syncDBs {
syncDB.PkgCache().ForEach(...)
}
```
**Rationale**:
- O(n) iteration where n = total packages in DB (not n queries)
- Single map construction, O(1) lookups per state package
- libalpm iterators are already lazy, no additional overhead
### 4. Dry-run behavior
**Decision**: Dry-run outputs exact packages that would be installed/removed without making any system changes.
**Implementation**:
- Skip `pacman -Syu` call
- Skip `pacman -D --asdeps` (mark all installed as deps)
- Skip `pacman -D --asexplicit` (mark state packages as explicit)
- Skip `pacman -Rns` orphan cleanup
- Still compute what WOULD happen for output
**Note on marking strategy**:
Instead of diffing between before/after installed packages, we simply:
1. After sync completes, run `pacman -D --asdeps` on ALL currently installed packages (this marks everything as deps)
2. Then run `pacman -D --asexplicit` on the collected state packages (this overrides them to explicit)
This is simpler and achieves the same result.
## Risks / Trade-offs
1. **[Risk]** dyalpm initialization requires root privileges
- **[Mitigation]** This is same as pacman itself; if user can't run pacman, declpac won't work
2. **[Risk]** libalpm state becomes stale if another pacman instance runs concurrently
- **[Mitigation]** Use proper locking, rely on pacman's own locking mechanism
3. **[Risk]** AUR packages still built sequentially
- **[Acceptable]** Parallel AUR builds out of scope for this change
4. **[Risk]** Memory usage for large package lists
- **[Mitigation]** Package map is ~100 bytes per package; 10k packages = ~1MB
## Migration Plan
1. Add `github.com/Jguer/dyalpm` to go.mod
2. Refactor `ValidatePackage()` to use dyalpm instead of subprocesses
3. Add `Installed bool` to `PackageInfo` struct
4. Implement new resolution algorithm in `categorizePackages()`
5. Update `Sync()` and `DryRun()` to use new algorithm
6. Test with various package combinations
7. Verify output matches previous behavior
## Open Questions
- **Q**: Should we also use dyalpm for `GetInstalledPackages()`?
- **A**: Yes, can use localDB.PkgCache().Collect() or iterate - aligns with overall approach
@@ -0,0 +1,35 @@
## Why
The current pacman implementation spawns multiple subprocesses per package (pacman -Qip, pacman -Sip) to check if packages exist in local/sync DBs or AUR. With many packages, this creates significant overhead. Using the Jguer/dyalpm library provides direct libalpm access for batch queries, eliminating subprocess overhead while maintaining the batched AUR HTTP calls.
## What Changes
- **Add dyalpm dependency**: Integrate Jguer/dyalpm library for direct libalpm access
- **Batch local DB check**: Use `localDB.PkgCache()` to check all packages at once instead of per-package `pacman -Qip`
- **Batch sync DB check**: Use `syncDBs[i].PkgCache()` to check all sync repos at once instead of per-package `pacman -Sip`
- **Enhance PackageInfo**: Add `Installed bool` field to track if package is already installed
- **New algorithm**: Implement unified package resolution flow:
1. Batch check local DB for all packages
2. Batch check sync DBs for remaining packages
3. Batch query AUR for non-found packages
4. Track installed status throughout
5. Perform sync operations with proper marking
6. Output summary of changes
## Capabilities
### New Capabilities
- `batch-package-resolution`: Unified algorithm that batch-resolves packages from local DB → sync DBs → AUR with proper installed tracking
- `dry-run-simulation`: Shows exact packages that would be installed/removed without making changes
### Modified Capabilities
- None - this is a pure optimization with no behavior changes visible to users
## Impact
- **Code**: `pkg/pacman/pacman.go` - refactored to use dyalpm
- **Dependencies**: Add Jguer/dyalpm to go.mod
- **APIs**: `ValidatePackage()` signature changes (returns installed status)
- **Performance**: O(n) subprocess calls → O(1) for local/sync DB checks
@@ -0,0 +1,72 @@
## ADDED Requirements
### Requirement: Batch package resolution from local, sync, and AUR databases
The system SHALL resolve packages in a single pass through local DB → sync DBs → AUR using batch operations to minimize subprocess/API calls.
#### Scenario: Package exists in local DB
- **WHEN** a package from collected state exists in the local database
- **THEN** the system SHALL mark it as found, set `Installed=true`, and exclude it from AUR queries
#### Scenario: Package exists in sync DB
- **WHEN** a package from collected state does NOT exist in local DB but exists in ANY enabled sync database
- **THEN** the system SHALL mark it as found, set `Installed=false`, and exclude it from AUR queries
#### Scenario: Package exists only in AUR
- **WHEN** a package from collected state does NOT exist in local or sync databases but exists in AUR
- **THEN** the system SHALL mark it as found with `InAUR=true`, set `Installed=false`, and use the cached AUR info
#### Scenario: Package not found anywhere
- **WHEN** a package from collected state is NOT in local DB, NOT in any sync DB, and NOT in AUR
- **THEN** the system SHALL return an error listing the unfound package(s)
#### Scenario: Batch AUR query
- **WHEN** multiple packages need AUR lookup
- **THEN** the system SHALL make a SINGLE HTTP request to AUR RPC with all package names (existing behavior preserved)
### Requirement: Efficient local DB lookup using dyalpm
The system SHALL use dyalpm's `PkgCache()` iterator to build a lookup map in O(n) time, where n is total packages in local DB, instead of O(n*m) subprocess calls.
#### Scenario: Build local package map
- **WHEN** initializing package resolution
- **THEN** the system SHALL iterate localDB.PkgCache() once and store all package names in a map for O(1) lookups
#### Scenario: Check package in local map
- **WHEN** checking if a package exists in local DB
- **THEN** the system SHALL perform an O(1) map lookup instead of spawning a subprocess
### Requirement: Efficient sync DB lookup using dyalpm
The system SHALL use each sync DB's `PkgCache()` iterator to check packages across all enabled repositories.
#### Scenario: Check package in sync DBs
- **WHEN** a package is not found in local DB
- **THEN** the system SHALL check all enabled sync databases using their iterators
#### Scenario: Package found in multiple sync repos
- **WHEN** a package exists in more than one sync repository (e.g., core and community)
- **THEN** the system SHALL use the first match found
### Requirement: Track installed status in PackageInfo
The system SHALL include an `Installed bool` field in `PackageInfo` to indicate whether the package is currently installed.
#### Scenario: Package is installed
- **WHEN** a package exists in the local database
- **THEN** `PackageInfo.Installed` SHALL be `true`
#### Scenario: Package is not installed
- **WHEN** a package exists only in sync DB or AUR (not in local DB)
- **THEN** `PackageInfo.Installed` SHALL be `false`
### Requirement: Mark installed packages as deps, then state packages as explicit
After package sync completes, the system SHALL mark all installed packages as dependencies, then override the collected state packages to be explicit. This avoids diffing before/after states.
#### Scenario: Mark all installed as deps
- **WHEN** package sync has completed (non-dry-run)
- **THEN** the system SHALL run `pacman -D --asdeps` to mark ALL currently installed packages as dependencies
#### Scenario: Override state packages to explicit
- **WHEN** all installed packages have been marked as deps
- **THEN** the system SHALL run `pacman -D --asexplicit` on the collected state packages, overriding their dependency status
#### Scenario: Dry-run skips marking
- **WHEN** operating in dry-run mode
- **THEN** the system SHALL NOT execute any `pacman -D` marking operations
@@ -0,0 +1,28 @@
## ADDED Requirements
### Requirement: Dry-run shows packages to install without making changes
In dry-run mode, the system SHALL compute what WOULD happen without executing any pacman operations.
#### Scenario: Dry-run lists packages to install
- **WHEN** dry-run is enabled and packages need to be installed
- **THEN** the system SHALL populate `Result.ToInstall` with all packages that would be installed (both sync and AUR)
#### Scenario: Dry-run lists packages to remove
- **WHEN** dry-run is enabled and orphan packages exist
- **THEN** the system SHALL populate `Result.ToRemove` with the list of orphan packages and `Result.Removed` with the count
#### Scenario: Dry-run skips pacman sync
- **WHEN** dry-run is enabled
- **THEN** the system SHALL NOT execute `pacman -Syu` for package installation
#### Scenario: Dry-run skips explicit/deps marking
- **WHEN** dry-run is enabled
- **THEN** the system SHALL NOT execute `pacman -D --asdeps` or `pacman -D --asexplicit`
#### Scenario: Dry-run skips orphan cleanup
- **WHEN** dry-run is enabled
- **THEN** the system SHALL NOT execute `pacman -Rns` for orphan removal
#### Scenario: Dry-run outputs count summary
- **WHEN** dry-run is enabled
- **THEN** the system SHALL still compute and output `Result.Installed` and `Result.Removed` counts as if the operations had run
@@ -0,0 +1,51 @@
## 1. Setup
- [x] 1.1 Add `github.com/Jguer/dyalpm` to go.mod
- [x] 1.2 Run `go mod tidy` to fetch dependencies
## 2. Core Refactoring
- [x] 2.1 Update `PackageInfo` struct to add `Installed bool` field
- [x] 2.2 Create `Pac` struct with `alpm.Handle` instead of just aurCache
- [x] 2.3 Implement `NewPac()` that initializes alpm handle and local/sync DBs
## 3. Package Resolution Algorithm
- [x] 3.1 Implement `buildLocalPkgMap()` - iterate localDB.PkgCache() to create lookup map
- [x] 3.2 Implement `checkSyncDBs()` - iterate each sync DB's PkgCache() to find packages
- [x] 3.3 Implement `resolvePackages()` - unified algorithm:
- Step 1: Check local DB for all packages (batch)
- Step 2: Check sync DBs for remaining packages (batch per repo)
- Step 3: Batch query AUR for remaining packages
- Step 4: Return error if any package unfound
- Step 5: Track installed status from local DB
## 4. Sync and DryRun Integration
- [x] 4.1 Refactor `Sync()` function to use new resolution algorithm
- [x] 4.2 Refactor `DryRun()` function to use new resolution algorithm
- [x] 4.3 Preserve AUR batched HTTP calls (existing `fetchAURInfo`)
- [x] 4.4 Preserve orphan cleanup logic (`CleanupOrphans()`)
## 5. Marking Operations
- [x] 5.1 Keep `MarkExplicit()` for marking state packages
- [x] 5.2 After sync, run `pacman -D --asdeps` on ALL installed packages (simplifies tracking)
- [x] 5.3 After deps marking, run `pacman -D --asexplicit` on collected state packages (overrides deps)
- [x] 5.4 Skip marking operations in dry-run mode
## 6. Cleanup and Output
- [x] 6.1 Remove subprocess-based `ValidatePackage()` implementation
- [x] 6.2 Remove subprocess-based `GetInstalledPackages()` implementation
- [x] 6.3 Update output summary to show installed/removed counts
- [x] 6.4 In dry-run mode, populate `ToInstall` and `ToRemove` lists
## 7. Testing
- [ ] 7.1 Test with packages in local DB only
- [ ] 7.2 Test with packages in sync DBs only
- [ ] 7.3 Test with AUR packages
- [ ] 7.4 Test with missing packages (should error)
- [ ] 7.5 Test dry-run mode output
- [ ] 7.6 Test orphan detection and cleanup
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-13
@@ -0,0 +1,146 @@
## Context
Pacman, Arch Linux's package manager, lacks declarative package management and
doesn't support partial upgrades. Users managing system packages from scripts
or configuration files need a way to ensure their system state matches a declared
package list, with all packages at their latest available versions.
## Goals / Non-Goals
**Goals:**
- Provide a declarative interface for pacman package management through
command-line interface
- Support flexible input sources (stdin and multiple state files) with
additive merging
- **Automatically resolve transitive dependencies (users specify only direct packages)**
- Enforce full package upgrades (no partial upgrades) to prevent version
mismatches
- Generate machine-readable output suitable for automated scripting
- Enable AI-driven implementation for new Go developers
**Non-Goals:**
- Package dependency resolution or smart upgrade scheduling
- Transaction rollback capabilities
- Pretty terminal output or interactive prompts
- Support for partial upgrades or selective package versions
- State persistence (tool only syncs, doesn't save state)
## Decisions
**Language: Go**
- Rationale: User wants modern CLI tool with native package management
capabilities
- Go provides strong standard library, easy command-line parsing, and
excellent for system-level operations
- User unfamiliar with Go - implementation will be AI-driven with detailed
documentation
**Input Parsing Approach**
- Parse whitespace, tabs, newlines for package names
- Support multiple --state files: all additive including with stdin
- Empty state detection: print error to stderr and exit with code 1 (abort)
**Package Database Freshness**
- Use libalpm (dyalpm) to check last sync time from /var/lib/pacman/db.lock timestamp
- If database older than 1 day, run `pacman -Syy` to refresh before validation
- Refresh happens before validation and sync operations
**Package Validation**
- Validate all declared packages exist in pacman repos or AUR before sync
- Use libalpm (dyalpm) for pacman repo queries (fast local DB access)
- Use Jguer/aur library for AUR queries
- Fail fast with clear error if any package not found
- Let pacman report detailed errors during sync (don't duplicate validation)
**State Merging Logic**
- All inputs are additive: combine all packages from stdin and all state files
- No conflict resolution: missing packages are added, duplicates accumulate
- Order-independent: inputs don't override each other
**Hybrid Query/Modify Approach**
- **Query operations**: Use libalpm (dyalpm) for fast local package database access
- Query installed packages, available packages, package details
- Check database freshness (last sync time)
- **Modify operations**: Use os/exec.Command to run pacman commands
- pacman -Syy: Refresh package databases
- pacman -Syu: Install/upgrade packages (full upgrade)
- pacman -D --explicit: Mark packages as explicitly installed
- pacman -Rns: Remove orphaned packages
- Capture stderr/stdout for error reporting and output generation
- Detect pacman exit codes and translate to tool exit codes
**Error Handling Strategy**
- Parse pacman error messages from stderr output
- Distinguish between package-not-found (warning) vs. execution failures (errors)
- Return appropriate exit codes: 0 for success, non-zero for errors
- Include error details in output for scripting purposes
**AUR Integration**
- First attempt: Try pacman -Syu for all packages (includes AUR auto-install if enabled)
- For packages not found in pacman repos: Batch query AUR via info endpoint (single HTTP request for multiple packages)
- If package in AUR: Build and install with makepkg (no AUR helpers)
- AUR packages should also upgrade to latest version (no partial updates)
- Clone AUR git repo to temp directory
- Run `makepkg -si` in temp directory for installation
- Capture stdout/stderr for output parsing
- Report error to stderr if package not found in pacman or AUR
**Dependency Resolution**
- Use pacman's dependency resolution by passing all declared packages to `pacman -Syu`
- Pacman automatically resolves and includes transitive dependencies
- For AUR packages, makepkg handles dependency resolution during build
- No custom dependency resolution logic required - delegate to pacman/makepkg
**Explicit Marking & Orphan Cleanup**
- Before syncing, get list of all currently installed packages
- Mark declared state packages as explicitly installed via `pacman -D --explicit <pkg>`
- All other installed packages remain as non-explicit (dependencies)
- After sync completes, run `pacman -Rns $(pacman -Qdtq)` to remove orphaned packages
- Pacman -Rns $(pacman -Qdtq) only removes packages that are not explicitly
installed and not required
- Capture and report number of packages removed during cleanup
**Package Version Management**
- Always force full system upgrade via `pacman -Syu <packages>`
- Pacman handles version selection automatically, ensuring latest versions
- No semantic version comparison or pinning logic required
**CLI Interface**
- Usage: `declpac --state file1.txt --state file2.txt < stdin`
- Multiple --state flags allowed, all additive
- Stdin input via standard input stream
- No interactive prompts - fully automated
- `--dry-run`: Simulate sync without making changes, print what would be installed/removed
**Output Format**
- Success: Print to stdout: `Installed X packages, removed Y packages`
- No changes: Print `Installed 0 packages, removed 0 packages`
- Dry-run: Print `Installed X packages, removed Y packages` with `Would install: ...` and `Would remove: ...` lines
- Errors: Print error message to stderr
- Exit codes: 0 for success, 1 for errors
## Risks / Trade-offs
**Known Risks:**
- [User unfamiliarity with Go] → Mitigation: Provide complete implementation
with detailed comments; user can review and run without understanding deeply
- [Pacman error message parsing complexity] → Mitigation: Use broad error
pattern matching; include error output as-is for debugging
- [Empty state triggers abort] → Mitigation: Print error to stderr and exit
with code 1 instead of proceeding with empty state
- [Additive merging could accumulate duplicates] → Mitigation: Accept as
design choice; pacman handles duplicates gracefully
- [Dependency conflicts could occur] → Mitigation: Let pacman handle standard
conflicts; tool won't implement complex dependency resolution
- [libalpm integration complexity] → Mitigation: Use dyalpm wrapper library;
validate queries work before build
- [AUR package build failures] → Mitigation: Capture makepkg output, report errors
to stderr; don't retry
**Trade-offs:**
- No conflict resolution: simpler merging but may include packages the system
rejects
- Additive vs replacement: safer but less predictable for users expecting
replacements
- No user prompts: fully automated for scripting but could be risky without
warnings
@@ -0,0 +1,40 @@
## Why
Pacman doesn't support declarative package management or partial upgrades,
making it challenging to maintain consistent system states. This tool provides
a clean, scriptable interface to synchronize the system with a declared package
list, ensuring all packages are at the latest version.
## What Changes
- Add CLI tool for declarative pacman package management
- Support stdin input for package lists
- Support multiple --state file inputs
- Merge all inputs (additive strategy with empty state warning)
- Force full upgrade of all packages (no partial upgrades)
- Handle transitive dependencies automatically (users only specify direct packages)
- Mark non-state packages as non-explicit before sync
- After sync, remove orphaned packages (cleanup)
- Support AUR packages: try pacman first, then makepkg, then report errors
- Machine-readable output (install/remove counts, exit codes)
- No conflict resolution for missing packages (append-only)
- Print error to stderr for empty state input and exit with code 1
- Dry-run mode: simulate sync without making changes, show what would be installed/removed
## Capabilities
### New Capabilities
- **stdin-input**: Read package lists from standard input stream
- **state-files**: Load declarative package states from files
- **input-merging**: Combine multiple input sources (stdin + multiple --state files)
- **state-validation**: Validate package names and empty states
- **transitive-deps**: Automatically resolve all transitive dependencies
- **pacman-sync**: Execute pacman operations to match declared state
- **aur-sync**: Handle AUR packages: pacman first, then fall back to makepkg,
then report errors
- **machine-output**: Generate machine-readable sync results for scripting
### Modified Capabilities
None (new tool)
@@ -0,0 +1,30 @@
## ADDED Requirements
### Requirement: Handle AUR packages with fallback and upgrade
The system SHALL first attempt to install packages via pacman, then fall back
to AUR for packages not found in repos, and upgrade AUR packages to
latest versions.
#### Scenario: Try pacman first
- **WHEN** package is in pacman repositories
- **THEN** install via pacman -Syu
#### Scenario: Fall back to AUR
- **WHEN** package is not in pacman repositories but is in AUR
- **THEN** batch query AUR via info endpoint (multiple packages in single request)
- **AND** build and install with makepkg -si
#### Scenario: Upgrade AUR packages
- **WHEN** AUR package is already installed but outdated
- **THEN** rebuild and reinstall with makepkg to get latest version
#### Scenario: Report error for missing packages
- **WHEN** package is not in pacman repositories or AUR
- **THEN** print error to stderr with package name
- **AND** exit with code 1
#### Scenario: AUR build failure
- **WHEN** makepkg fails to build package
- **THEN** print makepkg error to stderr
- **AND** exit with code 1
@@ -0,0 +1,49 @@
# Dry-Run Mode
## Summary
Add `--dry-run` flag to simulate the sync operation without making any changes
to the system. Shows what packages would be installed and what would be removed.
## Motivation
Users want to preview the effects of a sync operation before committing changes.
This is useful for:
- Verifying the intended changes are correct
- Avoiding unintended package installations
- Understanding what orphan cleanup will remove
## Interface
```
declpac --dry-run --state packages.txt
```
## Behavior
1. Read state files and stdin (same as normal mode)
2. Validate packages exist (same as normal mode)
3. Query current installed packages via `pacman -Qq`
4. Compare declared packages to current state
5. Identify packages that would be installed (not currently installed)
6. Identify orphans that would be removed via `pacman -Qdtq`
7. Output results with "Would install:" and "Would remove:" sections
## Output Format
```
Installed 3 packages, removed 2 packages
Would install: vim, git, docker
Would remove: python2, perl-xml-parser
```
## Non-Goals
- Actual package operations (no pacman -Syu, no pacman -Rns execution)
- Package version comparison
- Detailed dependency analysis
## Trade-offs
- Doesn't predict transitive dependencies that pacman might install
- Orphan list may change after packages are installed
@@ -0,0 +1,23 @@
## ADDED Requirements
### Requirement: Can merge multiple input sources
The system SHALL combine package lists from all inputs using an additive
strategy without conflict resolution.
#### Scenario: Additive merging of all inputs
- **WHEN** stdin and multiple state files provide package lists
- **THEN** system shall include all packages from all inputs in the final state
#### Scenario: Input priority is last-writer-wins per file
- **WHEN** multiple state files contain the same package name
- **THEN** the package from the last provided file in command-line order takes
precedence
#### Scenario: Missing packages from stdin are added
- **WHEN** stdin contains packages not in state files
- **THEN** those packages shall be added to the final state
#### Scenario: Duplicate packages are deduplicated
- **WHEN** the same package appears in multiple inputs
- **THEN** it shall be included once in the final state (map deduplication)
@@ -0,0 +1,30 @@
## ADDED Requirements
### Requirement: Generate consistent output format
The system SHALL produce console output suitable for logging and scripting.
#### Scenario: Successful sync with changes
- **WHEN** sync completes with packages installed or removed
- **THEN** print to stdout: `Installed X packages, removed Y packages`
- **AND** exit with code 0
#### Scenario: No changes needed
- **WHEN** all packages already match declared state
- **THEN** print to stdout: `Installed 0 packages, removed 0 packages`
- **AND** exit with code 0
#### Scenario: Error during sync
- **WHEN** pacman or makepkg operation fails
- **THEN** print error to stderr with details
- **AND** exit with code 1
#### Scenario: Validation failure
- **WHEN** package validation fails (package not in pacman or AUR)
- **THEN** print error to stderr with package name
- **AND** exit with code 1
#### Scenario: Empty state input
- **WHEN** no packages provided from stdin or state files
- **THEN** print error to stderr: `empty state input`
- **AND** exit with code 1
@@ -0,0 +1,29 @@
## ADDED Requirements
### Requirement: Can mark non-state packages as non-explicit
The system SHALL mark all packages not declared in the state as non-explicit
(dependencies) before syncing, so they can be safely removed later.
#### Scenario: Mark packages as non-explicit
- **WHEN** packages are declared in state
- **THEN** system shall mark those packages as explicitly installed via pacman -D --explicit
- **AND** mark all other installed packages as non-explicit
### Requirement: Can clean up orphaned packages
After syncing, the system SHALL remove packages that are no longer required
(as dependencies of removed packages).
#### Scenario: Orphan cleanup after sync
- **WHEN** sync operation completes successfully
- **THEN** system shall run pacman -Rns to remove unneeded dependencies
- **AND** report the number of packages removed
#### Scenario: Orphan cleanup respects explicitly installed
- **WHEN** a package not in state is marked as explicitly installed by user
- **THEN** system shall NOT remove it during orphan cleanup
#### Scenario: No orphans to clean
- **WHEN** there are no orphaned packages to remove
- **THEN** system shall report "No packages to remove" in output
@@ -0,0 +1,22 @@
## ADDED Requirements
### Requirement: Query pacman package database via libalpm
The system SHALL use libalpm for fast local queries against the pacman
package database.
#### Scenario: Query installed packages
- **WHEN** needing list of currently installed packages
- **THEN** query via libalpm alpm_list_get(ALPM_LIST_PACKAGES)
#### Scenario: Query available packages
- **WHEN** validating package exists in pacman repos
- **THEN** query via libalpm alpm_db_get_pkg()
#### Scenario: Check database last sync time
- **WHEN** checking if database needs refresh
- **THEN** check /var/lib/pacman/db.lock timestamp
#### Scenario: Query foreign packages
- **WHEN** checking if package is AUR-installed
- **THEN** query via libalpm alpm_db_get_pkg(ALPM_DB_TYPE_LOCAL)
@@ -0,0 +1,25 @@
## ADDED Requirements
### Requirement: Can sync system with declared package state
The system SHALL execute pacman operations to install and upgrade all declared packages.
#### Scenario: Full system upgrade with all packages
- **WHEN** system has declared package list
- **THEN** system shall run pacman -Syu with all declared package names
#### Scenario: No partial upgrades
- **WHEN** running pacman commands
- **THEN** system shall use -S --needed flag (sync with skip if up-to-date) ensuring declared packages are present
#### Scenario: Package availability check
- **WHEN** a package from input is not in pacman repositories
- **THEN** system shall report the error and include package name
#### Scenario: Pacman execution capture
- **WHEN** pacman commands execute
- **THEN** system shall capture both stdout and stderr for error reporting
#### Scenario: Exit code propagation
- **WHEN** pacman commands execute
- **THEN** system shall exit with code equal to pacman's exit code for success/failure detection
@@ -0,0 +1,21 @@
## ADDED Requirements
### Requirement: Can load package states from state files
The system SHALL load package lists from state files specified via --state flag.
#### Scenario: Single state file loading
- **WHEN** a state file is provided via --state flag
- **THEN** system shall read package names from the file content
#### Scenario: Multiple state file loading
- **WHEN** multiple --state flags are provided
- **THEN** system shall load package names from each file
#### Scenario: State file format is text list
- **WHEN** loading from state files
- **THEN** system shall parse whitespace-delimited package names
#### Scenario: File path validation
- **WHEN** a state file path points to a non-existent file
- **THEN** system shall report a file read error
@@ -0,0 +1,28 @@
## ADDED Requirements
### Requirement: Validate package names exist in pacman or AUR
The system SHALL validate that all declared packages exist in pacman
repositories or AUR before attempting to sync.
#### Scenario: Empty state detection
- **WHEN** no package names are found in stdin or state files
- **THEN** print error to stderr: `empty state input`
- **AND** exit with code 1
#### Scenario: Validate package in pacman repos
- **WHEN** package exists in pacman repositories
- **THEN** validation passes
#### Scenario: Validate package in AUR
- **WHEN** package not in pacman repos but exists in AUR
- **THEN** validation passes
#### Scenario: Package not found
- **WHEN** package not in pacman repos or AUR
- **THEN** print error to stderr with package name
- **AND** exit with code 1
#### Scenario: Database freshness check
- **WHEN** pacman database last sync was more than 1 day ago
- **THEN** run pacman -Syy to refresh before validation
@@ -0,0 +1,17 @@
## ADDED Requirements
### Requirement: Can read package lists from stdin
The system SHALL accept package names from standard input, where each line represents a package name.
#### Scenario: Packages from stdin are read correctly
- **WHEN** package names are passed via stdin separated by whitespace, tabs, or newlines
- **THEN** system shall parse each unique package name from the input stream
#### Scenario: Empty stdin input is handled
- **WHEN** stdin contains no package names
- **THEN** system shall skip stdin input processing
#### Scenario: Whitespace normalization
- **WHEN** packages are separated by multiple spaces, tabs, or newlines
- **THEN** each package name shall have leading/trailing whitespace trimmed
@@ -0,0 +1,23 @@
## ADDED Requirements
### Requirement: Can resolve transitive dependencies automatically
The system SHALL automatically determine and install all transitive dependencies
for declared packages, so users only need to specify direct packages.
#### Scenario: Transitive dependencies are resolved
- **WHEN** a package with dependencies is declared in state
- **THEN** system shall identify all transitive dependencies via pacman
- **AND** include them in the installation operation
#### Scenario: Dependency resolution includes AUR dependencies
- **WHEN** a package's transitive dependencies include AUR packages
- **THEN** system shall resolve those AUR dependencies via makepkg
#### Scenario: Shared dependencies are deduplicated
- **WHEN** multiple declared packages share dependencies
- **THEN** system shall install each shared dependency only once
#### Scenario: Missing dependency handling
- **WHEN** a declared package has a dependency not available in pacman or AUR
- **THEN** system shall report error for the missing dependency
@@ -0,0 +1,104 @@
## 1. Project Setup
- [x] 1.1 Initialize Go module with proper imports
- [x] 1.2 Add required dependencies (dyalpm wrapper, Jguer/aur)
- [x] 1.3 Set up project structure (cmd/declpac/main.go, pkg/ subdirectory)
- [x] 1.4 Add libalpm initialization and handle
## 2. Input Parsing
- [x] 2.1 Implement stdin reader to collect package names
- [x] 2.2 Implement state file reader for text-list format
- [x] 2.3 Add whitespace normalization for package names
- [x] 2.4 Create package name set data structure
## 3. Input Merging
- [x] 3.1 Implement additive merging of stdin and state file packages
- [x] 3.2 Handle multiple --state flags with last-writer-wins per file
- [x] 3.3 Implement duplicate package handling (no deduplication)
## 4. State Validation
- [x] 4.1 Implement empty state detection (no packages found)
- [x] 4.2 Add stderr error output for empty state
- [x] 4.3 Set exit code 1 for empty state case (abort, not proceed)
- [x] 4.4 Check pacman DB freshness (db.lock timestamp)
- [x] 4.5 Run pacman -Syy if DB older than 1 day
- [x] 4.6 Validate packages via libalpm (pacman repos)
- [x] 4.7 Validate packages via Jguer/aur (AUR)
- [x] 4.8 Fail fast with error if package not found
## 5. Pacman Integration (Hybrid: query via libalpm, modify via exec)
- [x] 5.1 Initialize libalpm handle for queries
- [x] 5.2 Implement libalpm query for installed packages
- [x] 5.3 Implement libalpm query for available packages
- [x] 5.4 Implement pacman -Syy command execution (DB refresh)
- [x] 5.5 Implement pacman -Syu command execution wrapper
- [x] 5.6 Add command-line argument construction with package list
- [x] 5.7 Capture pacman stdout and stderr output
- [x] 5.8 Implement pacman error message parsing
- [x] 5.9 Handle pacman exit codes for success/failure detection
- [x] 5.10 Verify pacman automatically resolves transitive dependencies
## 6. Explicit Marking & Orphan Cleanup
- [x] 6.1 Get list of currently installed packages before sync
- [x] 6.2 Mark declared state packages as explicitly installed via pacman -D --explicit
- [x] 6.3 Run pacman sync operation (5.x series)
- [x] 6.4 Run pacman -Rsu to remove orphaned packages
- [x] 6.5 Capture and report number of packages removed
- [x] 6.6 Handle case where no orphans exist (no packages removed)
## 7. AUR Integration
- [x] 7.1 Implement AUR package lookup via Jguer/aur library
- [x] 7.2 Check package not in pacman repos first (via libalpm)
- [x] 7.3 Query AUR for missing packages
- [x] 7.4 Implement AUR fallback using makepkg (direct build, not AUR helper)
- [x] 7.5 Clone AUR package git repo to temp directory
- [x] 7.6 Run makepkg -si in temp directory for installation
- [x] 7.7 Upgrade existing AUR packages to latest (makepkg rebuild)
- [x] 7.8 Add stderr error reporting for packages not in pacman or AUR
- [x] 7.9 Capture makepkg stdout and stderr for output parsing
- [x] 7.10 Handle makepkg exit codes for success/failure detection
## 8. Output Generation
- [x] 8.1 Parse pacman output for installed package count
- [x] 8.2 Parse pacman output for removed package count (orphan cleanup)
- [x] 8.3 Generate output: `Installed X packages, removed Y packages`
- [x] 8.4 Handle 0 packages case: `Installed 0 packages, removed 0 packages`
- [x] 8.5 Print errors to stderr
- [x] 8.6 Set exit code 0 for success, 1 for errors
## 9. CLI Interface
- [x] 9.1 Implement --state flag argument parsing
- [x] 9.2 Implement stdin input handling from /dev/stdin
- [x] 9.3 Set up correct CLI usage/help message
- [x] 9.4 Implement flag order validation
## 10. Integration & Testing
- [x] 10.1 Wire together stdin -> state files -> merging -> validation -> pacman sync -> orphan cleanup -> output
- [x] 10.2 Test empty state error output and exit code 1
- [x] 10.3 Test single state file parsing
- [x] 10.4 Test multiple state file merging
- [x] 10.5 Test stdin input parsing
- [x] 10.6 Test explicit marking before sync
- [x] 10.7 Test pacman command execution with real packages
- [x] 10.8 Test orphan cleanup removes unneeded packages
- [x] 10.9 Test AUR fallback with makepkg for AUR package
- [x] 10.10 Test error handling for missing packages
- [x] 10.11 Generate final binary
## 11. Dry-Run Mode
- [x] 11.1 Add --dry-run flag to CLI argument parsing
- [x] 11.2 Implement DryRun function to query current state
- [x] 11.3 Compare declared packages to current installations
- [x] 11.4 Identify packages to install (not currently installed)
- [x] 11.5 Identify orphans to remove via pacman -Qdtq
- [x] 11.6 Output "Would install:" and "Would remove:" sections
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-17
@@ -0,0 +1,44 @@
## Context
Current `Resolve` in `pkg/fetch/fetch.go`:
1. Initialize all packages with `Exists: true`
2. Check local DB, set `Installed: true` if found
3. Check sync DBs, set `Installed: false`
4. Check AUR, set `InAUR: true`
5. Error if not found anywhere
Bug: wrong default + wrong order + conflates two concerns.
## Goals / Non-Goals
**Goals:**
- Fix Resolve algorithm to correctly classify packages
- Separate "available" from "installed" concerns
**Non-Goals:**
- No new APIs or features
- No refactoring outside Resolve function
## Decisions
**Decision 1: Start with Exists: false**
- Default `Exists: false` (unknown) instead of `true`
- Only set true when confirmed in sync DBs
**Decision 2: Check sync DBs first**
- Order: sync → local → AUR
- First determine if package exists (anywhere)
- Then determine if installed (local only)
**Decision 3: Two-phase classification**
| Phase | Check | Sets |
|------|-------|-----|
| 1. Availability | sync DBs | Exists: true/false |
| 2. Installation | local DB | Installed: true/false |
| 3. Fallback | AUR | InAUR: true if not in sync |
## Risks / Trade-offs
- Minimal risk: localized fix only
- Trade-off: slight performance cost (check sync before local) - acceptable for correctness
@@ -0,0 +1,26 @@
## Why
The `Resolve` function in `pkg/fetch/fetch.go` has incorrect logic flow. It
initializes all packages with `Exists: true`, then checks local DB first, then
sync DBs, then AUR. This wrong order causes incorrect package state
classification - packages that exist only in AUR may be incorrectly marked as
found.
## What Changes
- Fix initialization: packages should start with `Exists: false` (unknown), not `true`
- Fix order: check sync DBs BEFORE local DB to determine availability
- Separate independent concerns: "available" (sync/AUR) from "installed" (local)
- All packages must validate: either `Exists: true` or `InAUR: true`
## Capabilities
### New Capabilities
- `package-resolve-logic`: Correct resolution algorithm for pacman packages
### Modified Capabilities
- None
## Impact
- `pkg/fetch/fetch.go`: `Resolve` function
@@ -0,0 +1,7 @@
## 1. Fix Resolve Function Logic
- [x] 1.1 Change initialization: start packages with Exists: false instead of true
- [x] 1.2 Reorder to check sync DBs BEFORE local DB
- [x] 1.3 Separate availability check (sync DBs) from installation check (local DB)
- [x] 1.4 Add AUR fallback for packages not in sync DBs
- [x] 1.5 Validate all packages have either Exists or InAUR before returning
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-14
@@ -0,0 +1,25 @@
## Context
The dyalpm library's `SyncDBs()` returns an empty slice because it doesn't automatically load sync databases from pacman.conf. This causes `resolvePackages()` to skip the sync DB search entirely and fall through to checking the AUR, which fails for official repo packages.
## Goals / Non-Goals
**Goals:**
- Register sync databases so `resolvePackages()` can find official repo packages
**Non-Goals:**
- Modify package installation logic
- Add support for custom repositories
## Decisions
1. **Register each repo manually** - After `handle.SyncDBs()` returns empty, loop through known repos (core, extra, multilib) and call `handle.RegisterSyncDB()` for each.
2. **Use hardcoded repo list** - Arch Linux standard repos are core, extra, multilib. This matches pacman.conf.
3. **Silent failure for missing repos** - If a repo isn't configured in pacman.conf, `RegisterSyncDB` will return a valid but empty DB. Filter by checking if `PkgCache()` has any packages.
## Risks / Trade-offs
- Hardcoded repo names may need updating if Arch adds/removes standard repos → Low risk, rare change
- Repo registration could fail silently → Mitigated by checking PkgCache count
@@ -0,0 +1,21 @@
## Why
The program fails to find packages that exist in official repositories (like `cmake` in `extra`). The dyalpm library's `SyncDBs()` returns an empty list, so the code never searches the sync databases and falls through to checking the AUR, which also doesn't have the package.
## What Changes
- Register sync databases manually after initializing the dyalpm handle
- Loop through known repos (core, extra, multilib) and call `RegisterSyncDB` for each
- Handle the case where a repo might not be configured in pacman.conf
## Capabilities
### New Capabilities
- None - this is a bug fix to existing functionality
### Modified Capabilities
- None
## Impact
- `pkg/pacman/pacman.go`: Modify `New()` function to register sync DBs after getting them from the handle
@@ -0,0 +1,7 @@
## ADDED Requirements
No new requirements - this is a bug fix to existing package resolution functionality.
## MODIFIED Requirements
No modified requirements.
@@ -0,0 +1,10 @@
## 1. Implement sync DB registration
- [x] 1.1 Modify Pac struct to include a method for registering sync DBs
- [x] 1.2 In New(), after getting empty syncDBs from handle, loop through ["core", "extra", "multilib"] and call RegisterSyncDB for each
- [x] 1.3 Filter out repos that have no packages (not configured in pacman.conf)
## 2. Test the fix
- [x] 2.1 Run `./declpac --dry-run --state <(echo "cmake")` and verify it resolves cmake from extra repo
- [x] 2.2 Test with other official repo packages (e.g., "git" from extra, "base" from core)
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-14
@@ -0,0 +1,141 @@
# Design: Refactor pkg Into Modular Packages
## New Structure
```
pkg/
├── pacman/ # write operations only
│ └── pacman.go
├── fetch/ # package resolution (NEW)
│ └── fetch.go
├── validation/ # DB freshness (existing, expand)
│ └── validation.go
├── output/ # (unchanged)
│ └── output.go
├── merge/ # (unchanged)
│ └── merge.go
└── input/ # (unchanged)
└── input.go
```
## Package Responsibilities
### pkg/fetch (NEW)
```
type Fetcher struct {
handle dyalpm.Handle
localDB dyalpm.Database
syncDBs []dyalpm.Database
aurCache map[string]AURPackage
}
func New() *Fetcher
func (f *Fetcher) Close() error
func (f *Fetcher) Resolve(packages []string) (map[string]*PackageInfo, error)
func (f *Fetcher) ListOrphans() ([]string, error)
```
Extracted from `pacman.go`:
- `buildLocalPkgMap()`
- `checkSyncDBs()`
- `resolvePackages()`
- AUR cache (`ensureAURCache()`, `fetchAURInfo()`)
### pkg/pacman (REFACTORED)
```
func Sync(packages []string) (*output.Result, error)
func DryRun(packages []string) (*output.Result, error)
func MarkAsExplicit(packages []string) error
func MarkAllAsDeps() error
func CleanupOrphans() (int, error)
```
Write actions only:
- `Sync()` - calls `Fetcher` for resolution, then pacman commands
- `DryRun()` - calls `Fetcher.Resolve()` + `ListOrphans()`
- `MarkAsExplicit()`, `MarkAllAsDeps()`
- `CleanupOrphans()` - calls `ListOrphans()` then removes
### pkg/validation (REFACTORED)
```
func CheckDBFreshness() error
```
Keep as-is: checks lock file age, auto-synces if stale.
Remove from `pacman.go`:
- `IsDBFresh()` - replaced by `CheckDBFreshness()`
- `SyncDB()` - called by validation when stale
### Orphan Deduplication
```go
// In fetch/fetch.go
func (f *Fetcher) ListOrphans() ([]string, error) {
cmd := exec.Command("pacman", "-Qdtq")
// ...
}
// In pacman/pacman.go
func CleanupOrphans() (int, error) {
orphans, err := fetcher.ListOrphans() // reuse
if err != nil || len(orphans) == 0 {
return 0, nil
}
// ... remove
}
func DryRun(...) (*output.Result, error) {
orphans, err := fetcher.ListOrphans() // reuse
// ...
}
```
## Data Structures Move to fetch
```go
// In fetch/fetch.go
type PackageInfo struct {
Name string
InAUR bool
Exists bool
Installed bool
AURInfo *AURPackage
syncPkg dyalpm.Package
}
type AURResponse struct {
Results []AURPackage `json:"results"`
}
type AURPackage struct {
Name string `json:"Name"`
PackageBase string `json:"PackageBase"`
Version string `json:"Version"`
URL string `json:"URL"`
}
```
## Import Changes
`pkg/pacman/fetch.go` will need:
- `github.com/Jguer/dyalpm`
- `github.com/Riyyi/declpac/pkg/output`
`pkg/pacman/pacman.go` will need:
- `github.com/Riyyi/declpac/pkg/fetch`
- `github.com/Riyyi/declpac/pkg/output`
## Dependencies to Add
- New import: `pkg/fetch` in `pacman` package
## No Changes To
- CLI entry points
- `output.Result` struct
- `input.ReadPackages()`
- `merge.Merge()`
@@ -0,0 +1,30 @@
# Proposal: Refactor pkg Into Modular Packages
## Summary
Split monolithic `pkg/pacman/pacman.go` into focused packages: `fetch` (package resolution), `validation` (DB checks), and keep `pacman` for write actions only. Also deduplicate orphan detection logic.
## Motivation
`pacman.go` is 645 lines doing too much:
- Package resolution (local + sync DBs + AUR)
- DB freshness checks
- Pacman write operations (sync, mark, clean)
- Orphan listing/cleanup
This violates single responsibility. Hard to test, reason about, or reuse. Also has duplication:
- `validation.CheckDBFreshness()` and `pacman.IsDBFresh()` both check DB freshness
- `listOrphans()` and `CleanupOrphans()` duplicate orphan detection
## Scope
- Extract package resolution to new `pkg/fetch/`
- Move DB freshness to `pkg/validation/` (keep `CheckDBFreshness()`)
- Keep only write actions in `pkg/pacman/`
- Deduplicate orphan logic: one function for listing, reuse in cleanup and dry-run
## Out of Scope
- No new features
- No API changes to CLI
- No changes to `pkg/output/`, `pkg/merge/`, `pkg/input/`
@@ -0,0 +1,15 @@
# Scope
## In Scope
- Extract package resolution from pacman.go to pkg/fetch
- Deduplicate orphan listing
- Keep pacman write operations in pacman package
- Maintain existing CLI API
## Out of Scope
- New features
- New package management backends (e.g., libalpm alternatives)
- Config file changes
- State file format changes
@@ -0,0 +1,38 @@
# Tasks: Refactor pkg Into Modular Packages
## Phase 1: Create pkg/fetch
- [x] 1.1 Create `pkg/fetch/fetch.go`
- [x] 1.2 Move `AURResponse`, `AURPackage`, `PackageInfo` structs to fetch
- [x] 1.3 Move `buildLocalPkgMap()` to fetch as `Fetcher.buildLocalPkgMap()`
- [x] 1.4 Move `checkSyncDBs()` to fetch as `Fetcher.checkSyncDBs()`
- [x] 1.5 Move `resolvePackages()` to fetch as `Fetcher.Resolve()`
- [x] 1.6 Move AUR cache methods (`ensureAURCache`, `fetchAURInfo`) to fetch
- [x] 1.7 Add `New()` and `Close()` to Fetcher
- [x] 1.8 Add `ListOrphans()` to Fetcher
## Phase 2: Refactor pkg/pacman
- [x] 2.1 Remove from pacman.go (now in fetch):
- `buildLocalPkgMap()`
- `checkSyncDBs()`
- `resolvePackages()`
- `ensureAURCache()`
- `fetchAURInfo()`
- `AURResponse`, `AURPackage`, `PackageInfo` structs
- [x] 2.2 Remove `IsDBFresh()` and `SyncDB()` (use validation instead)
- [x] 2.3 Update imports in pacman.go to include fetch package
- [x] 2.4 Update `Sync()` to use `fetch.Fetcher` for resolution
- [x] 2.5 Update `DryRun()` to call `fetcher.ListOrphans()` instead of duplicate call
- [x] 2.6 Update `CleanupOrphans()` to call `fetcher.ListOrphans()` instead of duplicate call
## Phase 3: Clean Up Validation
- [x] 3.1 Keep `validation.CheckDBFreshness()` as-is
- [x] 3.2 Remove any remaining DB freshness duplication
## Phase 4: Verify
- [x] 4.1 Run tests (if any exist)
- [x] 4.2 Build: `go build ./...`
- [x] 4.3 Verify CLI still works: test dry-run, sync, orphan cleanup