@abyrd9/harbor-cli 1.1.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -10,9 +10,14 @@ A CLI tool for managing local development services with ease. Harbor helps you o
10
10
  - 🔄 **Dependency Management**: Automatically checks for required system dependencies
11
11
  - 🎯 **Multi-Language Support**: Works with Node.js, Go, Rust, Python, PHP, Java, and more
12
12
  - 🖥️ **Tmux Integration**: Professional terminal multiplexing for service management
13
+ - 🤖 **AI Agent Friendly**: Stream service logs to files for AI coding assistants to monitor
13
14
 
14
15
  ## Installation
15
16
 
17
+ ```bash
18
+ bun add -g @abyrd9/harbor-cli
19
+ ```
20
+
16
21
  ```bash
17
22
  npm install -g @abyrd9/harbor-cli
18
23
  ```
@@ -60,12 +65,15 @@ Create a dedicated configuration file:
60
65
  {
61
66
  "name": "frontend",
62
67
  "path": "./vite-frontend",
63
- "command": "npm run dev"
68
+ "command": "npm run dev",
69
+ "log": true
64
70
  },
65
71
  {
66
72
  "name": "api",
67
73
  "path": "./go-api",
68
- "command": "go run ."
74
+ "command": "go run .",
75
+ "log": true,
76
+ "maxLogLines": 500
69
77
  },
70
78
  {
71
79
  "name": "database",
@@ -202,6 +210,42 @@ Override auto-detected commands by editing your configuration:
202
210
  }
203
211
  ```
204
212
 
213
+ ### Service Logging for AI Agents
214
+
215
+ Enable logging to stream service output to files in `.harbor/`. This is particularly useful when working with AI coding assistants (like Opencode, Codex, or Claude) that can read log files to understand what's happening in your services.
216
+
217
+ ```json
218
+ {
219
+ "services": [
220
+ {
221
+ "name": "api",
222
+ "path": "./api",
223
+ "command": "go run .",
224
+ "log": true,
225
+ "maxLogLines": 500
226
+ },
227
+ {
228
+ "name": "frontend",
229
+ "path": "./frontend",
230
+ "command": "npm run dev",
231
+ "log": true
232
+ }
233
+ ]
234
+ }
235
+ ```
236
+
237
+ **Options:**
238
+ - `log`: `true` to enable logging (default: `false`)
239
+ - `maxLogLines`: Maximum lines to keep in log file (default: `1000`)
240
+
241
+ **Log files are:**
242
+ - Stored in `.harbor/` directory (automatically added to `.gitignore`)
243
+ - Named `{session}-{service}.log` (e.g., `local-dev-test-api.log`)
244
+ - Automatically trimmed to prevent unbounded growth
245
+ - Stripped of ANSI escape codes for clean, readable output
246
+
247
+ **Use case:** Point your AI assistant to the `.harbor/` folder so it can monitor service logs, spot errors, and understand runtime behavior while helping you develop.
248
+
205
249
  ### Before/After Scripts
206
250
  Run custom scripts before and after your services start:
207
251
 
package/dist/harbor ADDED
Binary file
package/dist/index.js CHANGED
@@ -511,6 +511,7 @@ async function runServices() {
511
511
  console.error('Error reading config:', err);
512
512
  process.exit(1);
513
513
  }
514
+ ensureLogSetup(config);
514
515
  // Execute before scripts
515
516
  try {
516
517
  await execute(config.before || [], 'before');
@@ -548,6 +549,27 @@ async function runServices() {
548
549
  });
549
550
  });
550
551
  }
552
+ function ensureLogSetup(config) {
553
+ const shouldLog = config.services.some((service) => service.log);
554
+ if (!shouldLog) {
555
+ return;
556
+ }
557
+ const harborDir = path.join(process.cwd(), '.harbor');
558
+ if (!fs.existsSync(harborDir)) {
559
+ fs.mkdirSync(harborDir, { recursive: true });
560
+ }
561
+ const gitignorePath = path.join(process.cwd(), '.gitignore');
562
+ if (!fs.existsSync(gitignorePath)) {
563
+ return;
564
+ }
565
+ const gitignore = fs.readFileSync(gitignorePath, 'utf-8');
566
+ if (gitignore.includes('.harbor')) {
567
+ return;
568
+ }
569
+ const needsNewline = gitignore.length > 0 && !gitignore.endsWith('\n');
570
+ const entry = `${needsNewline ? '\n' : ''}.harbor/\n`;
571
+ fs.appendFileSync(gitignorePath, entry, 'utf-8');
572
+ }
551
573
  // Get the package root directory
552
574
  function getPackageRoot() {
553
575
  const __filename = fileURLToPath(import.meta.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abyrd9/harbor-cli",
3
- "version": "1.1.1",
3
+ "version": "2.0.1",
4
4
  "description": "A CLI tool for orchestrating local development services in tmux sessions. Perfect for microservices and polyglot projects with automatic service discovery and before/after script support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "check": "bunx npm-check -u",
15
- "build": "tsc",
15
+ "build": "bunx tsc",
16
16
  "prepare": "bun run build",
17
17
  "start": "bun dist/index.js",
18
18
  "harbor": "bun dist/index.js",
@@ -49,14 +49,18 @@
49
49
  "harbor": {
50
50
  "services": [
51
51
  {
52
- "name": "vite-project",
53
- "path": "test-services/vite-project",
54
- "command": "npm run dev"
52
+ "name": "web",
53
+ "path": "test-services/web",
54
+ "command": "bun run dev",
55
+ "log": true,
56
+ "maxLogLines": 500
55
57
  },
56
58
  {
57
59
  "name": "go-api",
58
60
  "path": "test-services/go-api",
59
- "command": "go run ."
61
+ "command": "go run .",
62
+ "log": true,
63
+ "maxLogLines": 500
60
64
  }
61
65
  ]
62
66
  }
package/scripts/dev.sh CHANGED
@@ -6,6 +6,26 @@ if tmux has-session -t local-dev-test 2>/dev/null; then
6
6
  tmux kill-session -t local-dev-test
7
7
  fi
8
8
 
9
+ session_name="local-dev-test"
10
+ repo_root="$(pwd)"
11
+ max_log_lines=1000
12
+ log_pids=()
13
+
14
+ cleanup_logs() {
15
+ for pid in "${log_pids[@]}"; do
16
+ kill "$pid" 2>/dev/null
17
+ done
18
+ }
19
+
20
+ start_log_trim() {
21
+ local log_file="$1"
22
+ local max_lines="$2"
23
+ # Run trimmer inside tmux so it survives script exit
24
+ tmux send-keys -t "$session_name":0 "( while true; do sleep 5; if [ -f \"$log_file\" ]; then lines=\$(wc -l < \"$log_file\"); if [ \"\$lines\" -gt $max_lines ]; then tail -n $max_lines \"$log_file\" > \"${log_file}.tmp\" && mv \"${log_file}.tmp\" \"$log_file\"; fi; fi; done ) &" C-m
25
+ }
26
+
27
+ trap cleanup_logs EXIT
28
+
9
29
  # Function to get harbor config
10
30
  get_harbor_config() {
11
31
  if [ -f "harbor.json" ]; then
@@ -18,7 +38,7 @@ get_harbor_config() {
18
38
  }
19
39
 
20
40
  # Start a new tmux session named 'local-dev-test' and rename the initial window
21
- tmux new-session -d -s local-dev-test
41
+ tmux new-session -d -s "$session_name"
22
42
 
23
43
  # Set tmux options
24
44
  tmux set-option -g prefix C-a
@@ -77,31 +97,51 @@ tmux set-option -g 'status-format[0]' ''
77
97
 
78
98
  # Create a new window for the interactive shell
79
99
  echo "Creating window for interactive shell"
80
- tmux rename-window -t local-dev-test:0 'Terminal'
100
+ tmux rename-window -t "$session_name":0 'Terminal'
81
101
 
82
102
  window_index=1 # Start from index 1
83
103
 
104
+ if get_harbor_config | jq -e '.services[] | select(.log == true)' >/dev/null 2>&1; then
105
+ mkdir -p "$repo_root/.harbor"
106
+ rm -f "$repo_root/.harbor/${session_name}-"*.log
107
+ fi
108
+
84
109
  # Create windows dynamically based on harbor config
85
- get_harbor_config | jq -c '.services[]' | while read service; do
110
+ while read service; do
86
111
  name=$(echo $service | jq -r '.name')
87
112
  path=$(echo $service | jq -r '.path')
88
113
  command=$(echo $service | jq -r '.command')
114
+ log=$(echo $service | jq -r '.log // false')
115
+ service_max_lines=$(echo $service | jq -r '.maxLogLines // empty')
116
+
117
+ # Use service-specific maxLogLines or fall back to default
118
+ effective_max_lines="${service_max_lines:-$max_log_lines}"
89
119
 
90
120
  echo "Creating window for service: $name"
91
121
  echo "Path: $path"
92
122
  echo "Command: $command"
93
123
 
94
- tmux new-window -t local-dev-test:$window_index -n "$name"
95
- tmux send-keys -t local-dev-test:$window_index "cd $path && $command" C-m
124
+ if [ "$log" = "true" ]; then
125
+ log_file="$repo_root/.harbor/${session_name}-${name}.log"
126
+ : > "$log_file"
127
+ # Run command directly (not via send-keys) to avoid shell echo
128
+ # Strip ANSI escape sequences and control chars before writing to log file
129
+ tmux new-window -t "$session_name":$window_index -n "$name" "cd \"$path\" && $command 2>&1 | sed -u 's/\\x1b\\[[0-9;]*[mGKHJ]//g' | tee -a \"$log_file\"; exec bash"
130
+ # Start background process to trim logs if they get too large
131
+ start_log_trim "$log_file" "$effective_max_lines"
132
+ else
133
+ tmux new-window -t "$session_name":$window_index -n "$name"
134
+ tmux send-keys -t "$session_name":$window_index "cd \"$path\" && $command" C-m
135
+ fi
96
136
 
97
137
  ((window_index++))
98
- done
138
+ done < <(get_harbor_config | jq -c '.services[]')
99
139
 
100
140
  # Bind 'Home' key to switch to the terminal window
101
141
  tmux bind-key -n Home select-window -t :0
102
142
 
103
143
  # Select the terminal window
104
- tmux select-window -t local-dev-test:0
144
+ tmux select-window -t "$session_name":0
105
145
 
106
146
  # Attach to the tmux session
107
- tmux attach-session -t local-dev-test
147
+ tmux attach-session -t "$session_name"