@abyrd9/harbor-cli 2.1.1 → 2.3.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,225 @@ 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
+ harbor launch --name my-session # Use custom session name`)
332
+ .option('-d, --detach', 'Run in background without attaching (headless mode). Use "anchor" to attach later.')
333
+ .option('--headless', 'Alias for --detach')
334
+ .option('--name <name>', 'Override tmux session name from config')
335
+ .action(async (options) => {
272
336
  try {
273
337
  await checkDependencies();
274
- await runServices();
338
+ await runServices({ detach: options.detach || options.headless, name: options.name });
339
+ }
340
+ catch (err) {
341
+ console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
342
+ process.exit(1);
343
+ }
344
+ });
345
+ program.command('anchor')
346
+ .description(`Attach your terminal to a running Harbor tmux session.
347
+
348
+ WHEN TO USE: After starting services with 'launch -d' (headless mode).
349
+
350
+ WHAT IT DOES:
351
+ - Checks if a Harbor tmux session is running
352
+ - Attaches your terminal to it
353
+ - You can then switch between service tabs with Shift+Left/Right
354
+ - Press Ctrl+q to kill session, or detach with Ctrl+b then d
355
+
356
+ PREREQUISITES: Services must be running (started with 'harbor launch').
357
+
358
+ EXAMPLE:
359
+ harbor launch -d # Start in background
360
+ harbor anchor # Attach to see the services`)
361
+ .action(async () => {
362
+ try {
363
+ const config = await readHarborConfig();
364
+ const sessionName = config.sessionName || 'harbor';
365
+ // Check if session exists
366
+ const checkSession = spawn('tmux', ['has-session', '-t', sessionName], {
367
+ stdio: 'pipe',
368
+ });
369
+ await new Promise((resolve) => {
370
+ checkSession.on('close', (code) => {
371
+ if (code !== 0) {
372
+ console.log(`❌ No running Harbor session found (looking for: ${sessionName})`);
373
+ console.log('\nTo start services, run:');
374
+ console.log(' harbor launch');
375
+ process.exit(1);
376
+ }
377
+ resolve();
378
+ });
379
+ });
380
+ // Attach to the session
381
+ const attach = spawn('tmux', ['attach-session', '-t', sessionName], {
382
+ stdio: 'inherit',
383
+ });
384
+ attach.on('close', (code) => {
385
+ process.exit(code || 0);
386
+ });
387
+ }
388
+ catch (err) {
389
+ console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
390
+ process.exit(1);
391
+ }
392
+ });
393
+ program.command('scuttle')
394
+ .description(`Stop all running Harbor services by killing the tmux session.
395
+
396
+ WHEN TO USE: When you want to stop all services and free up resources.
397
+
398
+ WHAT IT DOES:
399
+ - Finds the running Harbor tmux session
400
+ - Kills the entire session (all service windows)
401
+ - All services stop immediately
402
+
403
+ SAFE TO RUN: If no session is running, it simply reports that and exits cleanly.
404
+
405
+ EXAMPLE:
406
+ harbor scuttle # Stop all services`)
407
+ .action(async () => {
408
+ try {
409
+ const config = await readHarborConfig();
410
+ const sessionName = config.sessionName || 'harbor';
411
+ // Check if session exists
412
+ const checkSession = spawn('tmux', ['has-session', '-t', sessionName], {
413
+ stdio: 'pipe',
414
+ });
415
+ const sessionExists = await new Promise((resolve) => {
416
+ checkSession.on('close', (code) => {
417
+ resolve(code === 0);
418
+ });
419
+ });
420
+ if (!sessionExists) {
421
+ console.log(`ℹ️ No running Harbor session found (looking for: ${sessionName})`);
422
+ process.exit(0);
423
+ }
424
+ // Kill the session
425
+ const killSession = spawn('tmux', ['kill-session', '-t', sessionName], {
426
+ stdio: 'inherit',
427
+ });
428
+ killSession.on('close', (code) => {
429
+ if (code === 0) {
430
+ console.log(`✅ Harbor session '${sessionName}' stopped`);
431
+ }
432
+ else {
433
+ console.log('❌ Failed to stop Harbor session');
434
+ }
435
+ process.exit(code || 0);
436
+ });
437
+ }
438
+ catch (err) {
439
+ console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
440
+ process.exit(1);
441
+ }
442
+ });
443
+ program.command('bearings')
444
+ .description(`Show status of running Harbor services and session information.
445
+
446
+ WHEN TO USE: To check if services are running, especially in headless mode.
447
+
448
+ WHAT IT SHOWS:
449
+ - Session name and running status
450
+ - List of service windows (with 📄 indicator if logging enabled)
451
+ - Log file paths and sizes
452
+ - Available commands (anchor, scuttle)
453
+
454
+ OUTPUT EXAMPLE:
455
+ ⚓ Harbor Status
456
+ Session: harbor
457
+ Status: Running ✓
458
+ Services: [0] Terminal, [1] web 📄, [2] api 📄
459
+ Logs: .harbor/harbor-web.log (1.2 KB)
460
+
461
+ SAFE TO RUN: Works whether services are running or not.
462
+
463
+ EXAMPLE:
464
+ harbor bearings # Check what's running`)
465
+ .action(async () => {
466
+ try {
467
+ const config = await readHarborConfig();
468
+ const sessionName = config.sessionName || 'harbor';
469
+ // Check if session exists
470
+ const checkSession = spawn('tmux', ['has-session', '-t', sessionName], {
471
+ stdio: 'pipe',
472
+ });
473
+ const sessionExists = await new Promise((resolve) => {
474
+ checkSession.on('close', (code) => {
475
+ resolve(code === 0);
476
+ });
477
+ });
478
+ if (!sessionExists) {
479
+ console.log(`\n⚓ Harbor Status\n`);
480
+ console.log(` Session: ${sessionName}`);
481
+ console.log(` Status: Not running\n`);
482
+ console.log(` To start services, run:`);
483
+ console.log(` harbor launch # interactive mode`);
484
+ console.log(` harbor launch -d # headless mode\n`);
485
+ process.exit(0);
486
+ }
487
+ // Get list of windows (services)
488
+ const listWindows = spawn('tmux', ['list-windows', '-t', sessionName, '-F', '#{window_index}|#{window_name}|#{pane_current_command}'], {
489
+ stdio: ['pipe', 'pipe', 'pipe'],
490
+ });
491
+ let windowOutput = '';
492
+ listWindows.stdout.on('data', (data) => {
493
+ windowOutput += data.toString();
494
+ });
495
+ await new Promise((resolve) => {
496
+ listWindows.on('close', () => resolve());
497
+ });
498
+ const windows = windowOutput.trim().split('\n').filter(Boolean);
499
+ console.log(`\n⚓ Harbor Status\n`);
500
+ console.log(` Session: ${sessionName}`);
501
+ console.log(` Status: Running ✓`);
502
+ console.log(` Windows: ${windows.length}\n`);
503
+ console.log(` Services:`);
504
+ for (const window of windows) {
505
+ const [index, name, cmd] = window.split('|');
506
+ const logFile = `.harbor/${sessionName}-${name}.log`;
507
+ const hasLog = fs.existsSync(path.join(process.cwd(), logFile));
508
+ const logIndicator = hasLog ? ` 📄` : '';
509
+ console.log(` [${index}] ${name}${logIndicator}`);
510
+ }
511
+ // Check for log files
512
+ const harborDir = path.join(process.cwd(), '.harbor');
513
+ if (fs.existsSync(harborDir)) {
514
+ const logFiles = fs.readdirSync(harborDir).filter(f => f.endsWith('.log'));
515
+ if (logFiles.length > 0) {
516
+ console.log(`\n Logs:`);
517
+ for (const logFile of logFiles) {
518
+ const logPath = path.join(harborDir, logFile);
519
+ const stats = fs.statSync(logPath);
520
+ const sizeKB = (stats.size / 1024).toFixed(1);
521
+ console.log(` .harbor/${logFile} (${sizeKB} KB)`);
522
+ }
523
+ }
524
+ }
525
+ console.log(`\n Commands:`);
526
+ console.log(` harbor anchor - Anchor to the session`);
527
+ console.log(` harbor scuttle - Stop all services\n`);
275
528
  }
276
529
  catch (err) {
277
530
  console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
@@ -526,7 +779,7 @@ async function execute(scripts, scriptType) {
526
779
  }
527
780
  console.log(`\n✅ All ${scriptType} scripts completed successfully`);
528
781
  }
529
- async function runServices() {
782
+ async function runServices(options = {}) {
530
783
  const hasHarborConfig = checkHasHarborConfig();
531
784
  if (!hasHarborConfig) {
532
785
  console.log('❌ No harbor configuration found');
@@ -553,7 +806,7 @@ async function runServices() {
553
806
  try {
554
807
  await execute(config.before || [], 'before');
555
808
  }
556
- catch (err) {
809
+ catch {
557
810
  console.error('❌ Before scripts failed, aborting launch');
558
811
  process.exit(1);
559
812
  }
@@ -561,10 +814,16 @@ async function runServices() {
561
814
  await ensureScriptsExist();
562
815
  // Execute the script directly using spawn to handle I/O streams
563
816
  const scriptPath = path.join(getScriptsDir(), 'dev.sh');
817
+ const env = {
818
+ ...process.env,
819
+ HARBOR_DETACH: options.detach ? '1' : '0',
820
+ HARBOR_SESSION_NAME: options.name || '',
821
+ };
564
822
  const command = spawn('bash', [scriptPath], {
565
823
  stdio: 'inherit', // This will pipe stdin/stdout/stderr to the parent process
824
+ env,
566
825
  });
567
- return new Promise((resolve, reject) => {
826
+ return new Promise((resolve) => {
568
827
  command.on('error', (err) => {
569
828
  console.error(`Error running dev.sh: ${err}`);
570
829
  process.exit(1);
@@ -579,7 +838,7 @@ async function runServices() {
579
838
  await execute(config.after || [], 'after');
580
839
  resolve();
581
840
  }
582
- catch (err) {
841
+ catch {
583
842
  console.error('❌ After scripts failed');
584
843
  process.exit(1);
585
844
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abyrd9/harbor-cli",
3
- "version": "2.1.1",
3
+ "version": "2.3.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 env, config, or use default
15
+ session_name="${HARBOR_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
@@ -98,11 +99,11 @@ tmux set-option -g status-style bg="#1c1917",fg="#a8a29e"
98
99
  tmux set-option -g status-left ""
99
100
  tmux set-option -g status-right "#[fg=#a8a29e]shift+←/→ switch · ctrl+q close · #[fg=white]%H:%M#[default]"
100
101
  tmux set-window-option -g window-status-current-format "\
101
- #[fg=#6366f1, bg=#1c1917]
102
- #[fg=#6366f1, bg=#1c1917, bold] #W
102
+ #[fg=#6366f1, bg=#1c1917] →\
103
+ #[fg=#6366f1, bg=#1c1917, bold] #W\
103
104
  #[fg=#6366f1, bg=#1c1917] "
104
105
  tmux set-window-option -g window-status-format "\
105
- #[fg=#a8a29e, bg=#1c1917]
106
+ #[fg=#a8a29e, bg=#1c1917] \
106
107
  #[fg=#a8a29e, bg=#1c1917] #W \
107
108
  #[fg=#a8a29e, bg=#1c1917] "
108
109
 
@@ -160,5 +161,23 @@ tmux bind-key -n Home select-window -t :0
160
161
  # Select the terminal window
161
162
  tmux select-window -t "$session_name":0
162
163
 
163
- # Attach to the tmux session
164
- 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