@abyrd9/harbor-cli 2.1.0 → 2.2.0

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/dist/index.js CHANGED
@@ -151,7 +151,7 @@ async function checkDependencies() {
151
151
  const instructions = getInstallInstructions(dep.name, osInfo);
152
152
  if (instructions.length > 0) {
153
153
  console.log(' Installation options:');
154
- instructions.forEach(instruction => console.log(instruction));
154
+ instructions.forEach(instruction => { console.log(instruction); });
155
155
  }
156
156
  else {
157
157
  console.log(` General instructions: ${dep.installMsg}`);
@@ -203,22 +203,35 @@ const possibleProjectFiles = [
203
203
  const program = new Command();
204
204
  program
205
205
  .name('harbor')
206
- .description(`A CLI tool for managing your project's local development services
206
+ .description(`A CLI tool for orchestrating multiple local development services in tmux.
207
207
 
208
- Harbor helps you manage multiple local development services with ease.
209
- It provides a simple way to configure and run your services in a tmux session.
208
+ WHAT IT DOES:
209
+ Harbor manages multiple services (APIs, frontends, databases) in a single tmux
210
+ session. It auto-discovers projects, starts them together, and provides logging.
210
211
 
211
- Features:
212
- Automatic service discovery for Node.js, Go, Rust, Python, PHP, Java
213
- ✅ Pre-stage commands for setup before main service starts
214
- ✅ Configurable tmux session names
215
- ✅ Service logging with file output for monitoring and debugging
216
- ✅ Before/after script execution
212
+ REQUIREMENTS:
213
+ - tmux (terminal multiplexer)
214
+ - jq (JSON processor)
217
215
 
218
- Available Commands:
219
- dock Initialize a new Harbor project by scanning directories
220
- moor Add new services to existing Harbor configuration
221
- launch Start all services in a tmux session with pre-stage commands`)
216
+ TYPICAL WORKFLOW:
217
+ 1. harbor dock # First time: scan and create harbor.json config
218
+ 2. harbor launch # Start all services in interactive tmux session
219
+ harbor launch -d # Or start in headless/background mode
220
+ 3. harbor bearings # Check status of running services
221
+ 4. harbor anchor # Attach to running session (if headless)
222
+ 5. harbor scuttle # Stop all services when done
223
+
224
+ CONFIGURATION:
225
+ Config is stored in harbor.json or package.json under "harbor" key.
226
+ Services can specify: name, path, command, log (boolean), maxLogLines.
227
+
228
+ COMMANDS:
229
+ dock Create new harbor.json by scanning for projects (package.json, go.mod, etc.)
230
+ moor Add newly discovered services to existing config
231
+ launch Start all configured services (use -d/--headless for background mode)
232
+ bearings Show status: running services, session info, log file locations
233
+ anchor Attach terminal to a running Harbor tmux session
234
+ scuttle Stop all services by killing the tmux session`)
222
235
  .version(packageJson.version)
223
236
  .action(async () => await checkDependencies())
224
237
  .addHelpCommand(false);
@@ -227,8 +240,21 @@ if (process.argv.length <= 2) {
227
240
  program.help();
228
241
  }
229
242
  program.command('dock')
230
- .description('Initialize Harbor config by auto-discovering services in your project')
231
- .option('-p, --path <path>', 'The path to the root of your project', './')
243
+ .description(`Initialize a new Harbor project by scanning directories for services.
244
+
245
+ WHEN TO USE: Run this first in a new project that has no harbor.json yet.
246
+
247
+ WHAT IT DOES:
248
+ - Scans subdirectories for project files (package.json, go.mod, Cargo.toml, etc.)
249
+ - Creates harbor.json with discovered services
250
+ - Auto-detects run commands (npm run dev, go run ., etc.)
251
+
252
+ PREREQUISITES: No existing harbor.json or harbor config in package.json.
253
+
254
+ EXAMPLE:
255
+ harbor dock # Scan current directory
256
+ harbor dock -p ./apps # Scan specific subdirectory`)
257
+ .option('-p, --path <path>', 'Directory to scan for service projects (scans subdirectories)', './')
232
258
  .action(async (options) => {
233
259
  try {
234
260
  await checkDependencies();
@@ -248,8 +274,21 @@ program.command('dock')
248
274
  }
249
275
  });
250
276
  program.command('moor')
251
- .description('Scan for and add new services to your existing Harbor configuration')
252
- .option('-p, --path <path>', 'The path to the root of your project', './')
277
+ .description(`Add newly discovered services to an existing Harbor configuration.
278
+
279
+ WHEN TO USE: Run when you've added new service directories to your project.
280
+
281
+ WHAT IT DOES:
282
+ - Scans for new project directories not already in config
283
+ - Adds them to existing harbor.json or package.json harbor config
284
+ - Skips directories already configured
285
+
286
+ PREREQUISITES: Existing harbor.json or harbor config in package.json (run 'dock' first).
287
+
288
+ EXAMPLE:
289
+ harbor moor # Scan and add new services
290
+ harbor moor -p ./apps # Scan specific subdirectory for new services`)
291
+ .option('-p, --path <path>', 'Directory to scan for new service projects', './')
253
292
  .action(async (options) => {
254
293
  try {
255
294
  await checkDependencies();
@@ -267,11 +306,223 @@ program.command('moor')
267
306
  }
268
307
  });
269
308
  program.command('launch')
270
- .description('Start all services in a tmux session with pre-stage commands')
271
- .action(async () => {
309
+ .description(`Start all configured services in a tmux session.
310
+
311
+ WHEN TO USE: Run to start your development environment.
312
+
313
+ WHAT IT DOES:
314
+ - Kills any existing Harbor tmux session
315
+ - Runs 'before' scripts if configured
316
+ - Creates tmux session with a window per service
317
+ - Starts each service with its configured command
318
+ - Enables logging to .harbor/*.log if log:true in config
319
+ - Runs 'after' scripts when session ends
320
+
321
+ MODES:
322
+ Interactive (default): Opens tmux session, use Shift+Left/Right to switch tabs
323
+ Headless (-d/--headless): Runs in background, use 'anchor' to attach later
324
+
325
+ PREREQUISITES: harbor.json or harbor config in package.json.
326
+
327
+ EXAMPLES:
328
+ harbor launch # Start and attach to tmux session
329
+ harbor launch -d # Start in background (headless mode)
330
+ harbor launch --headless # Same as -d`)
331
+ .option('-d, --detach', 'Run in background without attaching (headless mode). Use "anchor" to attach later.')
332
+ .option('--headless', 'Alias for --detach')
333
+ .action(async (options) => {
272
334
  try {
273
335
  await checkDependencies();
274
- await runServices();
336
+ await runServices({ detach: options.detach || options.headless });
337
+ }
338
+ catch (err) {
339
+ console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
340
+ process.exit(1);
341
+ }
342
+ });
343
+ program.command('anchor')
344
+ .description(`Attach your terminal to a running Harbor tmux session.
345
+
346
+ WHEN TO USE: After starting services with 'launch -d' (headless mode).
347
+
348
+ WHAT IT DOES:
349
+ - Checks if a Harbor tmux session is running
350
+ - Attaches your terminal to it
351
+ - You can then switch between service tabs with Shift+Left/Right
352
+ - Press Ctrl+q to kill session, or detach with Ctrl+b then d
353
+
354
+ PREREQUISITES: Services must be running (started with 'harbor launch').
355
+
356
+ EXAMPLE:
357
+ harbor launch -d # Start in background
358
+ harbor anchor # Attach to see the services`)
359
+ .action(async () => {
360
+ try {
361
+ const config = await readHarborConfig();
362
+ const sessionName = config.sessionName || 'harbor';
363
+ // Check if session exists
364
+ const checkSession = spawn('tmux', ['has-session', '-t', sessionName], {
365
+ stdio: 'pipe',
366
+ });
367
+ await new Promise((resolve) => {
368
+ checkSession.on('close', (code) => {
369
+ if (code !== 0) {
370
+ console.log(`❌ No running Harbor session found (looking for: ${sessionName})`);
371
+ console.log('\nTo start services, run:');
372
+ console.log(' harbor launch');
373
+ process.exit(1);
374
+ }
375
+ resolve();
376
+ });
377
+ });
378
+ // Attach to the session
379
+ const attach = spawn('tmux', ['attach-session', '-t', sessionName], {
380
+ stdio: 'inherit',
381
+ });
382
+ attach.on('close', (code) => {
383
+ process.exit(code || 0);
384
+ });
385
+ }
386
+ catch (err) {
387
+ console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
388
+ process.exit(1);
389
+ }
390
+ });
391
+ program.command('scuttle')
392
+ .description(`Stop all running Harbor services by killing the tmux session.
393
+
394
+ WHEN TO USE: When you want to stop all services and free up resources.
395
+
396
+ WHAT IT DOES:
397
+ - Finds the running Harbor tmux session
398
+ - Kills the entire session (all service windows)
399
+ - All services stop immediately
400
+
401
+ SAFE TO RUN: If no session is running, it simply reports that and exits cleanly.
402
+
403
+ EXAMPLE:
404
+ harbor scuttle # Stop all services`)
405
+ .action(async () => {
406
+ try {
407
+ const config = await readHarborConfig();
408
+ const sessionName = config.sessionName || 'harbor';
409
+ // Check if session exists
410
+ const checkSession = spawn('tmux', ['has-session', '-t', sessionName], {
411
+ stdio: 'pipe',
412
+ });
413
+ const sessionExists = await new Promise((resolve) => {
414
+ checkSession.on('close', (code) => {
415
+ resolve(code === 0);
416
+ });
417
+ });
418
+ if (!sessionExists) {
419
+ console.log(`ℹ️ No running Harbor session found (looking for: ${sessionName})`);
420
+ process.exit(0);
421
+ }
422
+ // Kill the session
423
+ const killSession = spawn('tmux', ['kill-session', '-t', sessionName], {
424
+ stdio: 'inherit',
425
+ });
426
+ killSession.on('close', (code) => {
427
+ if (code === 0) {
428
+ console.log(`✅ Harbor session '${sessionName}' stopped`);
429
+ }
430
+ else {
431
+ console.log('❌ Failed to stop Harbor session');
432
+ }
433
+ process.exit(code || 0);
434
+ });
435
+ }
436
+ catch (err) {
437
+ console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
438
+ process.exit(1);
439
+ }
440
+ });
441
+ program.command('bearings')
442
+ .description(`Show status of running Harbor services and session information.
443
+
444
+ WHEN TO USE: To check if services are running, especially in headless mode.
445
+
446
+ WHAT IT SHOWS:
447
+ - Session name and running status
448
+ - List of service windows (with 📄 indicator if logging enabled)
449
+ - Log file paths and sizes
450
+ - Available commands (anchor, scuttle)
451
+
452
+ OUTPUT EXAMPLE:
453
+ ⚓ Harbor Status
454
+ Session: harbor
455
+ Status: Running ✓
456
+ Services: [0] Terminal, [1] web 📄, [2] api 📄
457
+ Logs: .harbor/harbor-web.log (1.2 KB)
458
+
459
+ SAFE TO RUN: Works whether services are running or not.
460
+
461
+ EXAMPLE:
462
+ harbor bearings # Check what's running`)
463
+ .action(async () => {
464
+ try {
465
+ const config = await readHarborConfig();
466
+ const sessionName = config.sessionName || 'harbor';
467
+ // Check if session exists
468
+ const checkSession = spawn('tmux', ['has-session', '-t', sessionName], {
469
+ stdio: 'pipe',
470
+ });
471
+ const sessionExists = await new Promise((resolve) => {
472
+ checkSession.on('close', (code) => {
473
+ resolve(code === 0);
474
+ });
475
+ });
476
+ if (!sessionExists) {
477
+ console.log(`\n⚓ Harbor Status\n`);
478
+ console.log(` Session: ${sessionName}`);
479
+ console.log(` Status: Not running\n`);
480
+ console.log(` To start services, run:`);
481
+ console.log(` harbor launch # interactive mode`);
482
+ console.log(` harbor launch -d # headless mode\n`);
483
+ process.exit(0);
484
+ }
485
+ // Get list of windows (services)
486
+ const listWindows = spawn('tmux', ['list-windows', '-t', sessionName, '-F', '#{window_index}|#{window_name}|#{pane_current_command}'], {
487
+ stdio: ['pipe', 'pipe', 'pipe'],
488
+ });
489
+ let windowOutput = '';
490
+ listWindows.stdout.on('data', (data) => {
491
+ windowOutput += data.toString();
492
+ });
493
+ await new Promise((resolve) => {
494
+ listWindows.on('close', () => resolve());
495
+ });
496
+ const windows = windowOutput.trim().split('\n').filter(Boolean);
497
+ console.log(`\n⚓ Harbor Status\n`);
498
+ console.log(` Session: ${sessionName}`);
499
+ console.log(` Status: Running ✓`);
500
+ console.log(` Windows: ${windows.length}\n`);
501
+ console.log(` Services:`);
502
+ for (const window of windows) {
503
+ const [index, name, cmd] = window.split('|');
504
+ const logFile = `.harbor/${sessionName}-${name}.log`;
505
+ const hasLog = fs.existsSync(path.join(process.cwd(), logFile));
506
+ const logIndicator = hasLog ? ` 📄` : '';
507
+ console.log(` [${index}] ${name}${logIndicator}`);
508
+ }
509
+ // Check for log files
510
+ const harborDir = path.join(process.cwd(), '.harbor');
511
+ if (fs.existsSync(harborDir)) {
512
+ const logFiles = fs.readdirSync(harborDir).filter(f => f.endsWith('.log'));
513
+ if (logFiles.length > 0) {
514
+ console.log(`\n Logs:`);
515
+ for (const logFile of logFiles) {
516
+ const logPath = path.join(harborDir, logFile);
517
+ const stats = fs.statSync(logPath);
518
+ const sizeKB = (stats.size / 1024).toFixed(1);
519
+ console.log(` .harbor/${logFile} (${sizeKB} KB)`);
520
+ }
521
+ }
522
+ }
523
+ console.log(`\n Commands:`);
524
+ console.log(` harbor anchor - Anchor to the session`);
525
+ console.log(` harbor scuttle - Stop all services\n`);
275
526
  }
276
527
  catch (err) {
277
528
  console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
@@ -526,7 +777,7 @@ async function execute(scripts, scriptType) {
526
777
  }
527
778
  console.log(`\n✅ All ${scriptType} scripts completed successfully`);
528
779
  }
529
- async function runServices() {
780
+ async function runServices(options = {}) {
530
781
  const hasHarborConfig = checkHasHarborConfig();
531
782
  if (!hasHarborConfig) {
532
783
  console.log('❌ No harbor configuration found');
@@ -553,7 +804,7 @@ async function runServices() {
553
804
  try {
554
805
  await execute(config.before || [], 'before');
555
806
  }
556
- catch (err) {
807
+ catch {
557
808
  console.error('❌ Before scripts failed, aborting launch');
558
809
  process.exit(1);
559
810
  }
@@ -561,10 +812,15 @@ async function runServices() {
561
812
  await ensureScriptsExist();
562
813
  // Execute the script directly using spawn to handle I/O streams
563
814
  const scriptPath = path.join(getScriptsDir(), 'dev.sh');
815
+ const env = {
816
+ ...process.env,
817
+ HARBOR_DETACH: options.detach ? '1' : '0',
818
+ };
564
819
  const command = spawn('bash', [scriptPath], {
565
820
  stdio: 'inherit', // This will pipe stdin/stdout/stderr to the parent process
821
+ env,
566
822
  });
567
- return new Promise((resolve, reject) => {
823
+ return new Promise((resolve) => {
568
824
  command.on('error', (err) => {
569
825
  console.error(`Error running dev.sh: ${err}`);
570
826
  process.exit(1);
@@ -579,7 +835,7 @@ async function runServices() {
579
835
  await execute(config.after || [], 'after');
580
836
  resolve();
581
837
  }
582
- catch (err) {
838
+ catch {
583
839
  console.error('❌ After scripts failed');
584
840
  process.exit(1);
585
841
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abyrd9/harbor-cli",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
4
4
  "description": "A CLI tool for orchestrating local development services in a tmux session. Perfect for microservices and polyglot projects with automatic service discovery and before/after script support.",
5
5
  "type": "module",
6
6
  "bin": {
package/scripts/dev.sh CHANGED
@@ -1,12 +1,24 @@
1
1
  #!/bin/bash
2
2
 
3
+ # Function to get harbor config
4
+ get_harbor_config() {
5
+ if [ -f "harbor.json" ]; then
6
+ cat harbor.json
7
+ elif [ -f "package.json" ]; then
8
+ jq '.harbor' package.json
9
+ else
10
+ echo "{}"
11
+ fi
12
+ }
13
+
14
+ # Get session name from config or use default
15
+ session_name=$(get_harbor_config | jq -r '.sessionName // "harbor"')
16
+
3
17
  # Check if the session already exists and kill it
4
- if tmux has-session -t local-dev-test 2>/dev/null; then
5
- echo "Killing existing tmux session 'local-dev-test'"
6
- tmux kill-session -t local-dev-test
18
+ if tmux has-session -t "$session_name" 2>/dev/null; then
19
+ echo "Killing existing tmux session '$session_name'"
20
+ tmux kill-session -t "$session_name"
7
21
  fi
8
-
9
- session_name="local-dev-test"
10
22
  repo_root="$(pwd)"
11
23
  max_log_lines=1000
12
24
  log_pids=()
@@ -38,18 +50,7 @@ start_log_trim() {
38
50
 
39
51
  trap cleanup_logs EXIT
40
52
 
41
- # Function to get harbor config
42
- get_harbor_config() {
43
- if [ -f "harbor.json" ]; then
44
- cat harbor.json
45
- elif [ -f "package.json" ]; then
46
- jq '.harbor' package.json
47
- else
48
- echo "{}"
49
- fi
50
- }
51
-
52
- # Start a new tmux session named 'local-dev-test' and rename the initial window
53
+ # Start a new tmux session and rename the initial window
53
54
  tmux new-session -d -s "$session_name"
54
55
 
55
56
  # Set tmux options
@@ -59,6 +60,10 @@ tmux set-option -g mouse on
59
60
  tmux set-option -g history-limit 50000
60
61
  tmux set-window-option -g mode-keys vi
61
62
 
63
+ # Enable extended keys so modifier combinations (like Shift+Enter) pass through to applications
64
+ tmux set-option -g extended-keys on
65
+ tmux set-option -g xterm-keys on
66
+
62
67
  # Add binding to kill session with Ctrl+q
63
68
  tmux bind-key -n C-q kill-session
64
69
 
@@ -94,11 +99,11 @@ tmux set-option -g status-style bg="#1c1917",fg="#a8a29e"
94
99
  tmux set-option -g status-left ""
95
100
  tmux set-option -g status-right "#[fg=#a8a29e]shift+←/→ switch · ctrl+q close · #[fg=white]%H:%M#[default]"
96
101
  tmux set-window-option -g window-status-current-format "\
97
- #[fg=#6366f1, bg=#1c1917]
98
- #[fg=#6366f1, bg=#1c1917, bold] #W
102
+ #[fg=#6366f1, bg=#1c1917] →\
103
+ #[fg=#6366f1, bg=#1c1917, bold] #W\
99
104
  #[fg=#6366f1, bg=#1c1917] "
100
105
  tmux set-window-option -g window-status-format "\
101
- #[fg=#a8a29e, bg=#1c1917]
106
+ #[fg=#a8a29e, bg=#1c1917] \
102
107
  #[fg=#a8a29e, bg=#1c1917] #W \
103
108
  #[fg=#a8a29e, bg=#1c1917] "
104
109
 
@@ -156,5 +161,23 @@ tmux bind-key -n Home select-window -t :0
156
161
  # Select the terminal window
157
162
  tmux select-window -t "$session_name":0
158
163
 
159
- # Attach to the tmux session
160
- tmux attach-session -t "$session_name"
164
+ # Attach to the tmux session (unless running in detached/headless mode)
165
+ if [ "${HARBOR_DETACH:-0}" = "1" ]; then
166
+ echo ""
167
+ echo "🚀 Harbor services started in detached mode"
168
+ echo ""
169
+ echo " Session: $session_name"
170
+ echo " Services: $((window_index - 1))"
171
+ echo ""
172
+ echo " Commands:"
173
+ echo " harbor anchor - Anchor to the tmux session"
174
+ echo " harbor scuttle - Scuttle all services"
175
+ echo ""
176
+ if get_harbor_config | jq -e '.services[] | select(.log == true)' >/dev/null 2>&1; then
177
+ echo " Logs:"
178
+ echo " tail -f .harbor/${session_name}-<service>.log"
179
+ echo ""
180
+ fi
181
+ else
182
+ tmux attach-session -t "$session_name"
183
+ fi