Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
192b41e18b | ||
|
|
1c74373d6b | ||
|
|
5e9b2521bd | ||
|
|
eb6bb8fb76 | ||
|
|
0b834214cd | ||
|
|
5031fdc1f8 | ||
|
|
0fa0988666 |
+1
-1
@@ -32,4 +32,4 @@ go.work.sum
|
|||||||
# .vscode/
|
# .vscode/
|
||||||
|
|
||||||
# Binary output
|
# Binary output
|
||||||
declpac
|
/declpac
|
||||||
|
|||||||
@@ -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] $1
|
skill [name=make-commit] message: $1
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ Make a git commit, distinguishing between user and AI contributions.
|
|||||||
|
|
||||||
**Steps**
|
**Steps**
|
||||||
|
|
||||||
1. **Ask user if this commit is by them or by AI**
|
1. **[REQUIRED] 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,22 +27,23 @@ 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.
|
||||||
|
|
||||||
**Capitalization rule**: Commit message should start with a capital letter,
|
If thed user DID provide a message, format it into a proper commit message.
|
||||||
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
|
||||||
@@ -50,14 +51,7 @@ 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. **Get git user config**
|
4. **Make the commit**
|
||||||
|
|
||||||
```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.
|
||||||
|
|
||||||
@@ -69,7 +63,7 @@ Make a git commit, distinguishing between user and AI contributions.
|
|||||||
|
|
||||||
**If by AI:**
|
**If by AI:**
|
||||||
```bash
|
```bash
|
||||||
git -c user.name="<git-config-name>" -c user.email="<git-config-email>" commit -m "<message>" --author="AI Bot <ai@local>"
|
git 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)
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
# declpac
|
# declpac
|
||||||
|
|
||||||
`declpac` is a declarative package manager for Arch Linux that syncs your system
|
Declarative package manager for Arch Linux that syncs your system with a declared package list using pacman.
|
||||||
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
|
||||||
- **Automatic dependency resolution** — Pacman handles transitive dependencies
|
- Smart orphan cleanup — removes packages no longer needed
|
||||||
- **Smart orphan cleanup** — Removes packages no longer needed
|
- Explicit package tracking — marks declared packages as explicit
|
||||||
- **Explicit package tracking** — Marks your declared packages as explicit
|
- AUR support — builds and installs AUR packages automatically
|
||||||
- **AUR support** — Falls back to AUR for packages not in official repos
|
- Machine-readable output — perfect for scripting
|
||||||
- **Machine-readable output** — Perfect for scripting and automation
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@@ -28,14 +24,13 @@ sudo mv declpac /usr/local/bin/
|
|||||||
### Dependencies
|
### Dependencies
|
||||||
|
|
||||||
- Go 1.21+
|
- Go 1.21+
|
||||||
- pacman (system package manager)
|
- pacman
|
||||||
- aur (AUR helper, optional for AUR support)
|
- makepkg (for AUR support)
|
||||||
- Root privileges (required for pacman operations)
|
- git (for AUR support)
|
||||||
|
- Root privileges
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
### Basic Usage
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Single state file
|
# Single state file
|
||||||
sudo declpac --state packages.txt
|
sudo declpac --state packages.txt
|
||||||
@@ -45,119 +40,63 @@ 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
|
||||||
|
|
||||||
State files contain one package name per line:
|
One package name per line, lines beginning with `#` are comments:
|
||||||
|
|
||||||
```
|
```
|
||||||
bash
|
bash
|
||||||
vim
|
vim
|
||||||
git
|
git
|
||||||
docker
|
docker
|
||||||
|
# this is a comment
|
||||||
```
|
```
|
||||||
|
|
||||||
Lines are treated as package names with whitespace trimmed:
|
### Options
|
||||||
|
|
||||||
```
|
|
||||||
bash # bash
|
|
||||||
vim # vim
|
|
||||||
# comment # ignored
|
|
||||||
```
|
|
||||||
|
|
||||||
### Command Line Options
|
|
||||||
|
|
||||||
| Flag | Alias | Description |
|
| Flag | Alias | Description |
|
||||||
|------|-------|-------------|
|
|------|-------|-------------|
|
||||||
| `--state` | `-s` | State file(s) to read package list from (can be used multiple times) |
|
| `--state` | `-s` | State file to read package list from (can be used multiple times) |
|
||||||
| `--yes` | `-y` | Skip confirmation prompts (for scripting) |
|
| `--dry-run` | | Preview changes without applying them |
|
||||||
| `--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. **Collect packages** — Reads from all `--state` files and stdin
|
1. **Read** — Collect packages from all state files and stdin
|
||||||
2. **Merge** — Combines all packages additively (duplicates allowed)
|
2. **Merge** — Combine into single package list
|
||||||
3. **Validate** — Checks packages exist in repos or AUR
|
3. **Categorize** — Check if packages are in official repos or AUR
|
||||||
4. **Mark explicit** — Marks declared packages as explicit dependencies
|
4. **Sync** — Install/update packages via pacman
|
||||||
5. **Sync** — Runs `pacman -Syu` to install/upgrade packages
|
5. **Build** — Build and install AUR packages via makepkg
|
||||||
6. **Cleanup** — Removes orphaned packages with `pacman -Rns`
|
6. **Mark** — Mark declared packages as explicit, all others as dependencies
|
||||||
7. **Report** — Outputs summary: `Installed X packages, removed Y packages`
|
7. **Cleanup** — Remove orphaned packages
|
||||||
|
|
||||||
### Database Freshness
|
### Database Freshness
|
||||||
|
|
||||||
If the pacman database is older than 24 hours, `declpac` automatically refreshes it with `pacman -Syy` before validation.
|
If the pacman database is older than 24 hours, it is automatically refreshed.
|
||||||
|
|
||||||
### Orphan Cleanup
|
### Logging
|
||||||
|
|
||||||
After syncing, `declpac` identifies and removes packages that are:
|
Operations are logged to `/var/log/declpac.log`.
|
||||||
- Not explicitly installed
|
|
||||||
- Not required by any other package
|
|
||||||
|
|
||||||
This keeps your system clean from dependency artifacts.
|
## Output
|
||||||
|
|
||||||
## Output Format
|
|
||||||
|
|
||||||
```
|
```
|
||||||
# Success (packages installed/removed)
|
# Packages installed/removed
|
||||||
Installed 5 packages, removed 2 packages
|
Installed 5 packages, removed 2 packages
|
||||||
|
|
||||||
# Success (no changes)
|
# No changes needed
|
||||||
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>
|
||||||
```
|
```
|
||||||
@@ -167,36 +106,54 @@ error: package not found: <package-name>
|
|||||||
| Code | Meaning |
|
| Code | Meaning |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| 0 | Success |
|
| 0 | Success |
|
||||||
| 1 | Error (no packages, validation failure, pacman error) |
|
| 1 | Error |
|
||||||
|
|
||||||
## Security Considerations
|
## Examples
|
||||||
|
|
||||||
- **Run as root** — `declpac` requires root privileges for pacman operations
|
### Minimal System
|
||||||
- **Review state files** — Only install packages from trusted sources
|
|
||||||
- **Backup** — Consider backing up your system before major changes
|
```bash
|
||||||
|
echo -e "base\nbase-devel\nlinux-headers\nvim\ngit\ncurl" > ~/.config/declpac/minimal.txt
|
||||||
|
sudo declpac --state ~/.config/declpac/minimal.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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
|
||||||
```
|
```
|
||||||
@@ -210,11 +167,12 @@ declpac/
|
|||||||
├── pkg/
|
├── pkg/
|
||||||
│ ├── input/ # State file/stdin reading
|
│ ├── input/ # State file/stdin reading
|
||||||
│ ├── merge/ # Package merging
|
│ ├── merge/ # Package merging
|
||||||
│ ├── validation/ # Package validation
|
│ ├── fetch/ # Package resolution (pacman/AUR)
|
||||||
│ ├── pacman/ # Pacman integration
|
│ ├── pacman/ # Pacman operations
|
||||||
│ └── output/ # Output formatting
|
│ ├── validation/ # Database freshness check
|
||||||
├── go.mod # Go module
|
│ ├── output/ # Output formatting
|
||||||
└── README.md # This file
|
│ └── state/ # Logging
|
||||||
|
└── README.md
|
||||||
```
|
```
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|||||||
+7
-6
@@ -12,6 +12,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,12 +35,6 @@ 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",
|
||||||
@@ -57,6 +52,12 @@ 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")
|
||||||
|
|
||||||
|
|||||||
@@ -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`
|
||||||
+28
-31
@@ -188,6 +188,9 @@ 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 {
|
||||||
@@ -215,44 +218,38 @@ 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())
|
|
||||||
|
|
||||||
var notInSync []string
|
f.ensureAURCache(packages)
|
||||||
for _, pkg := range notInLocal {
|
|
||||||
if syncPkg, ok := syncPkgs[pkg]; ok {
|
for _, pkg := range packages {
|
||||||
result[pkg] = &PackageInfo{
|
info := result[pkg]
|
||||||
Name: pkg,
|
if info == nil {
|
||||||
Exists: true,
|
continue
|
||||||
InAUR: false,
|
|
||||||
Installed: false,
|
|
||||||
syncPkg: syncPkg,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
notInSync = append(notInSync, pkg)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if len(notInSync) > 0 {
|
if info.Installed {
|
||||||
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 {
|
||||||
result[pkg] = &PackageInfo{
|
info.InAUR = true
|
||||||
Name: pkg,
|
info.AURInfo = &aurInfo
|
||||||
Exists: true,
|
|
||||||
InAUR: true,
|
|
||||||
Installed: false,
|
|
||||||
AURInfo: &aurInfo,
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unfound = append(unfound, pkg)
|
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
if len(unfound) > 0 {
|
|
||||||
return nil, fmt.Errorf("package(s) not found: %s", strings.Join(unfound, ", "))
|
if syncPkg, ok := syncPkgs[pkg]; ok {
|
||||||
|
info.InAUR = false
|
||||||
|
info.Installed = false
|
||||||
|
info.syncPkg = syncPkg
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if aurInfo, ok := f.aurCache[pkg]; ok {
|
||||||
|
info.InAUR = true
|
||||||
|
info.Installed = false
|
||||||
|
info.AURInfo = &aurInfo
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("package not found: %s", pkg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+41
-15
@@ -2,6 +2,7 @@ package pacman
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"regexp"
|
"regexp"
|
||||||
@@ -10,6 +11,7 @@ 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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,9 +20,13 @@ 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")
|
||||||
cmd.Stdout = os.Stdout
|
state.Write([]byte("MarkAllAsDeps...\n"))
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
|
||||||
|
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
|
||||||
@@ -35,9 +41,13 @@ 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...)
|
||||||
cmd.Stdout = os.Stdout
|
state.Write([]byte("MarkAsExplicit...\n"))
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
|
||||||
|
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
|
||||||
@@ -157,19 +167,25 @@ 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 = os.Stdout
|
cloneCmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
|
||||||
cloneCmd.Stderr = os.Stderr
|
cloneCmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter())
|
||||||
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 = os.Stdout
|
makepkgCmd.Stdout = io.MultiWriter(os.Stdout, state.GetLogWriter())
|
||||||
makepkgCmd.Stderr = os.Stderr
|
makepkgCmd.Stderr = io.MultiWriter(os.Stderr, state.GetLogWriter())
|
||||||
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())
|
||||||
@@ -200,13 +216,19 @@ 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{"-Syu"}, packages...)
|
args := append([]string{"-S", "--needed"}, 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)
|
||||||
|
|
||||||
@@ -233,9 +255,15 @@ 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())
|
||||||
@@ -271,12 +299,10 @@ 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 _, installed := localPkgs[pkg]; !installed {
|
if info.InAUR {
|
||||||
if info.InAUR {
|
aurPkgs = append(aurPkgs, pkg)
|
||||||
aurPkgs = append(aurPkgs, pkg)
|
} else if _, installed := localPkgs[pkg]; !installed {
|
||||||
} else {
|
toInstall = append(toInstall, pkg)
|
||||||
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())
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user