Skip to content

Commit 0d51e83

Browse files
committed
snapshot
1 parent 2d67c7a commit 0d51e83

File tree

4 files changed

+135
-40
lines changed

4 files changed

+135
-40
lines changed

app/playbooks/60-second-linux.yaml

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
nixpkgs:
2-
version: "22.05"
2+
version: "380be19fbd2d9079f677978361792cb25e8a3635" # nixos-22.05 branch
33
packages:
44
- coreutils
55
- util-linux
@@ -17,25 +17,25 @@ system_prompt: |
1717
1818
When recommending actions, prioritize practical steps.
1919
commands:
20-
- command: 186m66li0jgnsx31rv524y2s871yrfcp-procps-4.0.4/bin/uptime
20+
- command: uptime
2121
description: System uptime and load averages
22-
- command: 186m66li0jgnsx31rv524y2s871yrfcp-procps-4.0.4/bin/vmstat 1
22+
- command: vmstat 1
2323
description: Virtual memory statistics, 1s updates
24-
- command: gj1lv5c9vh8vy76ad9h7qg9b34lx3fb5-sysstat-12.7.4/bin/mpstat -P ALL 1
24+
- command: mpstat -P ALL 1
2525
description: CPU utilization per core, 1s
26-
- command: gj1lv5c9vh8vy76ad9h7qg9b34lx3fb5-sysstat-12.7.4/bin/pidstat 1
26+
- command: pidstat 1
2727
description: Per-process CPU usage, 1s
28-
- command: gj1lv5c9vh8vy76ad9h7qg9b34lx3fb5-sysstat-12.7.4/bin/iostat -xz 1
28+
- command: iostat -xz 1
2929
description: Extended I/O statistics, 1s
30-
- command: 186m66li0jgnsx31rv524y2s871yrfcp-procps-4.0.4/bin/free -m
30+
- command: free -m
3131
description: Memory usage in MiB
32-
- command: gj1lv5c9vh8vy76ad9h7qg9b34lx3fb5-sysstat-12.7.4/bin/sar -n DEV 1
32+
- command: sar -n DEV 1
3333
description: Network device statistics, 1s
34-
- command: gj1lv5c9vh8vy76ad9h7qg9b34lx3fb5-sysstat-12.7.4/bin/sar -n TCP,ETCP 1
34+
- command: sar -n TCP,ETCP 1
3535
description: TCP counters and errors, 1s
36-
- command: 186m66li0jgnsx31rv524y2s871yrfcp-procps-4.0.4/bin/top -b -n 1
36+
- command: top -b -n 1
3737
description: Top processes snapshot
38-
- command: 5h222rfc23nmb8fzfwnwmnd3w2a2iz8f-util-linux-2.41-bin/bin/dmesg
38+
- command: dmesg
3939
description: Kernel ring buffer
4040
sudo: true
4141

app/toolbox.go

Lines changed: 78 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,18 +51,29 @@ func (t *Toolbox) Download() error {
5151
t.TempDir = tempDir
5252

5353
// Download the file
54-
resp, err := http.Get(t.URL)
55-
if err != nil {
56-
return fmt.Errorf("failed to download file: %w", err)
57-
}
58-
defer resp.Body.Close()
59-
60-
if resp.StatusCode != http.StatusOK {
61-
return fmt.Errorf("bad status: %s", resp.Status)
54+
var rc io.ReadCloser
55+
if strings.HasPrefix(t.URL, "file://") {
56+
localPath := strings.TrimPrefix(t.URL, "file://")
57+
file, err := os.Open(localPath)
58+
if err != nil {
59+
return fmt.Errorf("failed to open local file: %w", err)
60+
}
61+
rc = file
62+
} else {
63+
resp, err := http.Get(t.URL)
64+
if err != nil {
65+
return fmt.Errorf("failed to download file: %w", err)
66+
}
67+
if resp.StatusCode != http.StatusOK {
68+
resp.Body.Close()
69+
return fmt.Errorf("bad status: %s", resp.Status)
70+
}
71+
rc = resp.Body
6272
}
73+
defer rc.Close()
6374

6475
// Create XZ reader
65-
xzReader, err := xz.NewReader(resp.Body)
76+
xzReader, err := xz.NewReader(rc)
6677
if err != nil {
6778
return fmt.Errorf("failed to create XZ reader: %w", err)
6879
}
@@ -140,29 +151,69 @@ func (t *Toolbox) Cleanup() error {
140151
// PlaybookConfig is defined in playbook.go
141152

142153
// GetDiagnosticCommands returns the predefined diagnostic commands with actual toolbox paths
143-
func (t *Toolbox) GetDiagnosticCommands() []DiagnosticCommand {
154+
func (t *Toolbox) GetDiagnosticCommands() ([]DiagnosticCommand, error) {
144155
if t.TempDir == "" {
145156
// Return empty slice if toolbox not downloaded yet
146-
return []DiagnosticCommand{}
157+
return []DiagnosticCommand{}, nil
147158
}
148159

149160
cfg, err := loadDiagnosticsConfigEmbedded()
150161
if err != nil {
151-
return []DiagnosticCommand{}
162+
return []DiagnosticCommand{}, err
152163
}
153164
// Store playbook on toolbox for later use (e.g., system prompt)
154165
t.Playbook = cfg
155166

156167
toolboxPath := path.Join(t.TempDir, "toolbox")
157-
prefix := fmt.Sprintf("%s/proot -b %s/nix:/nix %s/nix/store/", toolboxPath, toolboxPath, toolboxPath)
168+
storeDir := filepath.Join(toolboxPath, "nix", "store")
169+
prootPath := filepath.Join(toolboxPath, "proot")
170+
prootPrefix := fmt.Sprintf("%s -b %s/nix:/nix", prootPath, toolboxPath)
158171

159172
var result []DiagnosticCommand
160173
for i := range cfg.Commands {
161174
c := cfg.Commands[i]
162-
cmdStr := prefix + c.Command
175+
parts := strings.Fields(c.Command)
176+
if len(parts) == 0 {
177+
return nil, fmt.Errorf("command '%s' is empty", c.Command)
178+
}
179+
binName := parts[0]
180+
args := parts[1:]
181+
182+
// Try to locate the binary inside toolbox nix store under */bin or */sbin
183+
resolved := ""
184+
if entries, err := os.ReadDir(storeDir); err == nil {
185+
for _, e := range entries {
186+
if !e.IsDir() {
187+
continue
188+
}
189+
candidate := filepath.Join(storeDir, e.Name(), "bin", binName)
190+
if st, err := os.Stat(candidate); err == nil && !st.IsDir() && (st.Mode()&0o111 != 0) {
191+
resolved = candidate
192+
break
193+
}
194+
candidate = filepath.Join(storeDir, e.Name(), "sbin", binName)
195+
if st, err := os.Stat(candidate); err == nil && !st.IsDir() && (st.Mode()&0o111 != 0) {
196+
resolved = candidate
197+
break
198+
}
199+
}
200+
}
201+
202+
var cmdStr string
203+
if resolved != "" {
204+
if len(args) > 0 {
205+
cmdStr = prootPrefix + " " + resolved + " " + strings.Join(args, " ")
206+
} else {
207+
cmdStr = prootPrefix + " " + resolved
208+
}
209+
} else {
210+
return nil, fmt.Errorf("binary for command '%s' not found in toolbox nix store", binName)
211+
}
212+
163213
if c.Sudo {
164214
cmdStr = "sudo " + cmdStr
165215
}
216+
166217
timeout := 5 * time.Second
167218
if c.TimeoutSeconds > 0 {
168219
timeout = time.Duration(c.TimeoutSeconds) * time.Second
@@ -174,7 +225,7 @@ func (t *Toolbox) GetDiagnosticCommands() []DiagnosticCommand {
174225
Timeout: timeout,
175226
})
176227
}
177-
return result
228+
return result, nil
178229
}
179230

180231
// loadDiagnosticsConfig reads diagnostics.yaml from the working directory or alongside the executable.
@@ -219,6 +270,14 @@ func (t *Toolbox) ExecuteDiagnosticCommand(cmd DiagnosticCommand) (string, error
219270
return "", fmt.Errorf("toolbox not downloaded yet")
220271
}
221272

273+
// If command resolution failed earlier, return a descriptive error now.
274+
if strings.TrimSpace(cmd.Command) == "" {
275+
if cmd.Spec != nil && cmd.Spec.Command != "" {
276+
return "", fmt.Errorf("binary for command '%s' not found in toolbox nix store", cmd.Spec.Command)
277+
}
278+
return "", fmt.Errorf("command binary not found in toolbox nix store")
279+
}
280+
222281
// Split the command into parts for exec.Command
223282
parts := strings.Fields(cmd.Command)
224283
if len(parts) == 0 {
@@ -253,7 +312,10 @@ func (t *Toolbox) ExecuteDiagnosticCommand(cmd DiagnosticCommand) (string, error
253312

254313
// RunSpecificDiagnosticCommand runs a specific diagnostic command by its display name
255314
func (t *Toolbox) RunSpecificDiagnosticCommand(displayName string) (string, error) {
256-
commands := t.GetDiagnosticCommands()
315+
commands, err := t.GetDiagnosticCommands()
316+
if err != nil {
317+
return "", err
318+
}
257319

258320
for _, cmd := range commands {
259321
if cmd.Display == displayName {

app/toolbox/main.go

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,9 @@ type playbookConfig struct {
2525
func main() {
2626
var yamlPath string
2727
var outPath string
28-
var workDir string
2928

3029
flag.StringVar(&yamlPath, "yaml", "", "Path to YAML with nixpkgs.packages")
3130
flag.StringVar(&outPath, "out", "toolbox.tar.xz", "Output archive path")
32-
flag.StringVar(&workDir, "workdir", ".", "Working directory where toolbox/ is created")
3331
flag.Parse()
3432

3533
if yamlPath == "" {
@@ -49,23 +47,30 @@ func main() {
4947
fatalf("no nixpkgs.packages listed in %s", yamlPath)
5048
}
5149

52-
toolboxDir, _ := filepath.Abs(filepath.Join(workDir, "toolbox"))
53-
_ = exec.Command("chmod", "-R", "u+w", toolboxDir).Run()
54-
_ = exec.Command("rm", "-rf", toolboxDir).Run()
55-
50+
workDir, err := os.MkdirTemp("", "toolbox_work_*")
51+
if err != nil {
52+
fatalf("failed to create temporary workdir: %v", err)
53+
}
5654
defer func() {
57-
_ = exec.Command("chmod", "-R", "u+w", toolboxDir).Run()
58-
_ = exec.Command("rm", "-rf", toolboxDir).Run()
55+
_ = exec.Command("chmod", "-R", "u+w", workDir).Run()
56+
_ = exec.Command("rm", "-rf", workDir).Run()
5957
}()
6058

61-
if err := nixCopy(toolboxDir, cfg.Nixpkgs.Packages); err != nil {
59+
toolboxDir, _ := filepath.Abs(filepath.Join(workDir, "toolbox"))
60+
if err := nixCopy(toolboxDir, cfg.Nixpkgs.Version, cfg.Nixpkgs.Packages); err != nil {
6261
fatalf("nix copy failed: %v", err)
6362
}
6463

6564
if err := fetchAndInstallProot(toolboxDir); err != nil {
6665
fatalf("failed to install proot: %v", err)
6766
}
6867

68+
// Include the playbook YAML inside the toolbox directory
69+
if err := copyFile(yamlPath, filepath.Join(toolboxDir, "playbook.yaml"), 0o644); err != nil {
70+
fatalf("failed to copy playbook YAML: %v", err)
71+
}
72+
73+
outPath, _ = filepath.Abs(outPath)
6974
if err := createTarXz(outPath, toolboxDir); err != nil {
7075
fatalf("failed to create tar.xz: %v", err)
7176
}
@@ -85,7 +90,7 @@ func readYAML(path string) (*playbookConfig, error) {
8590
return &cfg, nil
8691
}
8792

88-
func nixCopy(destDir string, pkgs []string) error {
93+
func nixCopy(destDir string, version string, pkgs []string) error {
8994
if _, err := exec.LookPath("nix"); err != nil {
9095
return fmt.Errorf("nix not found in PATH: %w", err)
9196
}
@@ -95,8 +100,15 @@ func nixCopy(destDir string, pkgs []string) error {
95100
"copy",
96101
"--to", destDir,
97102
}
103+
// Build flake reference. If a version (commit SHA) is provided, pin to that revision.
104+
// Otherwise, fall back to the registry alias "nixpkgs".
105+
flakeRef := "nixpkgs"
106+
if version != "" {
107+
// Expecting a commit SHA; use the GitHub flake URL form.
108+
flakeRef = "github:NixOS/nixpkgs/" + version
109+
}
98110
for _, p := range pkgs {
99-
args = append(args, "nixpkgs#"+p)
111+
args = append(args, flakeRef+"#"+p)
100112
}
101113
cmd := exec.Command("nix", args...)
102114
cmd.Stdout = os.Stdout
@@ -203,6 +215,27 @@ func copyExecutable(src, dst string) error {
203215
return dstF.Close()
204216
}
205217

218+
func copyFile(srcPath, dstPath string, perm os.FileMode) error {
219+
if err := os.MkdirAll(filepath.Dir(dstPath), 0o755); err != nil {
220+
return err
221+
}
222+
srcF, err := os.Open(srcPath)
223+
if err != nil {
224+
return err
225+
}
226+
defer srcF.Close()
227+
228+
dstF, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, perm)
229+
if err != nil {
230+
return err
231+
}
232+
if _, err := io.Copy(dstF, srcF); err != nil {
233+
dstF.Close()
234+
return err
235+
}
236+
return dstF.Close()
237+
}
238+
206239
func createTarXz(outPath string, dir string) error {
207240
parent := filepath.Dir(dir)
208241
base := filepath.Base(dir)

app/ui.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ type model struct {
8585
// NewModel constructs a model initialised with all diagnostic commands in a
8686
// pending state.
8787
func NewModel(tb *Toolbox) *model {
88-
cmds := tb.GetDiagnosticCommands()
88+
cmds, _ := tb.GetDiagnosticCommands()
8989
n := len(cmds)
9090

9191
vp := viewport.New(0, 0)
@@ -154,7 +154,7 @@ func (m *model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
154154
}
155155
m.downloaded = true
156156
// Populate commands now that toolbox is available
157-
m.commands = m.toolbox.GetDiagnosticCommands()
157+
m.commands, _ = m.toolbox.GetDiagnosticCommands()
158158
n := len(m.commands)
159159
m.statuses = make([]commandStatus, n)
160160
m.outputs = make([]string, n)

0 commit comments

Comments
 (0)