Skip to content

Commit 18f5247

Browse files
Refine chat archive resilience and verification
1 parent e274c5f commit 18f5247

20 files changed

Lines changed: 1005 additions & 151 deletions

.githooks/pre-commit

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env bash
2+
3+
set -euo pipefail
4+
5+
echo "Running chat archive validation"
6+
npm run ci
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
name: Chat Archive Quality
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- 'pkg/chatarchive/**'
7+
- 'internal/chatarchivecmd/**'
8+
- 'cmd/create/chat_archive.go'
9+
- 'cmd/backup/chats.go'
10+
- 'test/e2e/**'
11+
- 'scripts/chatarchive-ci.sh'
12+
- 'package.json'
13+
- '.githooks/pre-commit'
14+
- '.github/workflows/chatarchive-quality.yml'
15+
push:
16+
branches:
17+
- main
18+
- develop
19+
paths:
20+
- 'pkg/chatarchive/**'
21+
- 'internal/chatarchivecmd/**'
22+
- 'cmd/create/chat_archive.go'
23+
- 'cmd/backup/chats.go'
24+
- 'test/e2e/**'
25+
- 'scripts/chatarchive-ci.sh'
26+
- 'package.json'
27+
- '.githooks/pre-commit'
28+
- '.github/workflows/chatarchive-quality.yml'
29+
30+
jobs:
31+
chatarchive-ci:
32+
name: Chat Archive CI (${{ matrix.os }})
33+
runs-on: ${{ matrix.os }}
34+
strategy:
35+
fail-fast: false
36+
matrix:
37+
os: [ubuntu-latest, macos-latest, windows-latest]
38+
39+
steps:
40+
- name: Checkout
41+
uses: actions/checkout@v4
42+
43+
- name: Set up Go
44+
uses: actions/setup-go@v5
45+
with:
46+
go-version: '1.25'
47+
48+
- name: Set up Node
49+
uses: actions/setup-node@v4
50+
with:
51+
node-version: '20'
52+
53+
- name: Download Go modules
54+
run: go mod download
55+
shell: bash
56+
57+
- name: Run chat archive CI
58+
run: npm run ci
59+
shell: bash
60+
env:
61+
CHATARCHIVE_SKIP_E2E: ${{ matrix.os == 'windows-latest' && '1' || '0' }}
62+
63+
- name: Upload verification summary
64+
if: always()
65+
uses: actions/upload-artifact@v4
66+
with:
67+
name: chatarchive-summary-${{ matrix.os }}
68+
path: outputs/chatarchive-ci/
69+
70+
- name: Publish job summary
71+
if: always()
72+
shell: bash
73+
run: |
74+
if [[ -f outputs/chatarchive-ci/summary.txt ]]; then
75+
cat outputs/chatarchive-ci/summary.txt >> "$GITHUB_STEP_SUMMARY"
76+
fi

cmd/backup/chats.go

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,9 @@
66
package backup
77

88
import (
9-
"fmt"
10-
11-
"github.com/CodeMonkeyCybersecurity/eos/pkg/chatarchive"
9+
"github.com/CodeMonkeyCybersecurity/eos/internal/chatarchivecmd"
1210
eos "github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
1311
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
14-
"github.com/CodeMonkeyCybersecurity/eos/pkg/parse"
1512
"github.com/spf13/cobra"
1613
)
1714

@@ -24,39 +21,16 @@ and write an index manifest with duplicate mappings.
2421
This is a convenience alias for 'eos create chat-archive'.
2522
Works across Ubuntu, macOS, and Windows.
2623

27-
Examples:
24+
Examples:
2825
eos backup chats
2926
eos backup chats --source ~/.claude --source ~/dev
3027
eos backup chats --dry-run`,
3128
RunE: eos.Wrap(func(rc *eos_io.RuntimeContext, cmd *cobra.Command, _ []string) error {
32-
sources, _ := cmd.Flags().GetStringSlice("source")
33-
dest, _ := cmd.Flags().GetString("dest")
34-
dryRun, _ := cmd.Flags().GetBool("dry-run")
35-
36-
result, err := chatarchive.Archive(rc, chatarchive.Options{
37-
Sources: chatarchive.ExpandSources(sources),
38-
Dest: parse.ExpandHome(dest),
39-
DryRun: dryRun,
40-
})
41-
if err != nil {
42-
return err
43-
}
44-
45-
if dryRun {
46-
fmt.Printf("Dry run complete. %d unique files, %d duplicates.\n",
47-
result.UniqueFiles, result.Duplicates)
48-
} else {
49-
fmt.Printf("Archive complete. %d unique files copied, %d duplicates mapped.\n",
50-
result.UniqueFiles, result.Duplicates)
51-
fmt.Printf("Manifest: %s\n", result.ManifestPath)
52-
}
53-
return nil
29+
return chatarchivecmd.Run(rc, cmd)
5430
}),
5531
}
5632

5733
func init() {
5834
BackupCmd.AddCommand(chatsCmd)
59-
chatsCmd.Flags().StringSlice("source", chatarchive.DefaultSources(), "Source directories to scan")
60-
chatsCmd.Flags().String("dest", chatarchive.DefaultDest(), "Destination archive directory")
61-
chatsCmd.Flags().Bool("dry-run", false, "Show what would be archived without copying files")
35+
chatarchivecmd.BindFlags(chatsCmd)
6236
}

cmd/create/chat_archive.go

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,9 @@
66
package create
77

88
import (
9-
"fmt"
10-
11-
"github.com/CodeMonkeyCybersecurity/eos/pkg/chatarchive"
9+
"github.com/CodeMonkeyCybersecurity/eos/internal/chatarchivecmd"
1210
eos "github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
1311
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
14-
"github.com/CodeMonkeyCybersecurity/eos/pkg/parse"
1512
"github.com/spf13/cobra"
1613
)
1714

@@ -32,32 +29,9 @@ Examples:
3229

3330
func init() {
3431
CreateCmd.AddCommand(CreateChatArchiveCmd)
35-
CreateChatArchiveCmd.Flags().StringSlice("source", chatarchive.DefaultSources(), "Source directories to scan")
36-
CreateChatArchiveCmd.Flags().String("dest", chatarchive.DefaultDest(), "Destination archive directory")
37-
CreateChatArchiveCmd.Flags().Bool("dry-run", false, "Show what would be archived without copying files")
32+
chatarchivecmd.BindFlags(CreateChatArchiveCmd)
3833
}
3934

4035
func runCreateChatArchive(rc *eos_io.RuntimeContext, cmd *cobra.Command, _ []string) error {
41-
sources, _ := cmd.Flags().GetStringSlice("source")
42-
dest, _ := cmd.Flags().GetString("dest")
43-
dryRun, _ := cmd.Flags().GetBool("dry-run")
44-
45-
result, err := chatarchive.Archive(rc, chatarchive.Options{
46-
Sources: chatarchive.ExpandSources(sources),
47-
Dest: parse.ExpandHome(dest),
48-
DryRun: dryRun,
49-
})
50-
if err != nil {
51-
return err
52-
}
53-
54-
if dryRun {
55-
fmt.Printf("Dry run complete. %d unique files, %d duplicates.\n",
56-
result.UniqueFiles, result.Duplicates)
57-
} else {
58-
fmt.Printf("Archive complete. %d unique files copied, %d duplicates mapped.\n",
59-
result.UniqueFiles, result.Duplicates)
60-
fmt.Printf("Manifest: %s\n", result.ManifestPath)
61-
}
62-
return nil
36+
return chatarchivecmd.Run(rc, cmd)
6337
}

internal/chatarchivecmd/run.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package chatarchivecmd
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
8+
"github.com/CodeMonkeyCybersecurity/eos/pkg/chatarchive"
9+
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
10+
"github.com/spf13/cobra"
11+
"github.com/uptrace/opentelemetry-go-extra/otelzap"
12+
"go.uber.org/zap"
13+
)
14+
15+
func BindFlags(cmd *cobra.Command) {
16+
cmd.Flags().StringSlice("source", chatarchive.DefaultSources(), "Source directories to scan")
17+
cmd.Flags().String("dest", chatarchive.DefaultDest(), "Destination archive directory")
18+
cmd.Flags().Bool("dry-run", false, "Show what would be archived without copying files")
19+
}
20+
21+
func Run(rc *eos_io.RuntimeContext, cmd *cobra.Command) error {
22+
sources, _ := cmd.Flags().GetStringSlice("source")
23+
dest, _ := cmd.Flags().GetString("dest")
24+
dryRun, _ := cmd.Flags().GetBool("dry-run")
25+
26+
result, err := chatarchive.Archive(rc, chatarchive.Options{
27+
Sources: sources,
28+
Dest: dest,
29+
DryRun: dryRun,
30+
})
31+
if err != nil {
32+
return err
33+
}
34+
35+
logger := otelzap.Ctx(rc.Ctx)
36+
logger.Info("terminal prompt:", zap.String("output", formatSummary(result, dryRun)))
37+
for _, failure := range result.Failures {
38+
logger.Warn("Chat archive file failure",
39+
zap.String("path", failure.Path),
40+
zap.String("stage", failure.Stage),
41+
zap.String("reason", failure.Reason))
42+
}
43+
44+
return nil
45+
}
46+
47+
func formatSummary(result *chatarchive.Result, dryRun bool) string {
48+
lines := []string{
49+
statusLine(dryRun),
50+
fmt.Sprintf("Unique files: %d", result.UniqueFiles),
51+
fmt.Sprintf("Duplicates in this run: %d", result.Duplicates),
52+
fmt.Sprintf("Already archived: %d", result.Skipped),
53+
fmt.Sprintf("Empty files ignored: %d", result.EmptyFiles),
54+
fmt.Sprintf("File failures: %d", result.FailureCount),
55+
fmt.Sprintf("Duration: %s", result.Duration.Round(10*time.Millisecond)),
56+
}
57+
58+
if result.ManifestPath != "" {
59+
lines = append(lines, fmt.Sprintf("Manifest: %s", result.ManifestPath))
60+
}
61+
if result.RecoveredManifestPath != "" {
62+
lines = append(lines, fmt.Sprintf("Recovered corrupt manifest: %s", result.RecoveredManifestPath))
63+
}
64+
if result.FailureCount > len(result.Failures) {
65+
lines = append(lines, fmt.Sprintf("Additional failures not shown: %d", result.FailureCount-len(result.Failures)))
66+
}
67+
68+
return strings.Join(lines, "\n")
69+
}
70+
71+
func statusLine(dryRun bool) string {
72+
if dryRun {
73+
return "Dry run complete."
74+
}
75+
return "Archive complete."
76+
}

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"name": "eos",
3+
"private": true,
4+
"scripts": {
5+
"ci": "bash ./scripts/chatarchive-ci.sh"
6+
}
7+
}

0 commit comments

Comments
 (0)