@abyrd9/harbor-cli 2.3.2 → 2.3.3

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.
Files changed (3) hide show
  1. package/dist/index.js +208 -235
  2. package/package.json +3 -2
  3. package/scripts/dev.sh +50 -45
package/dist/index.js CHANGED
@@ -8,6 +8,20 @@ import { readFileSync } from 'node:fs';
8
8
  import { fileURLToPath } from 'node:url';
9
9
  import os from 'node:os';
10
10
  import readline from 'node:readline';
11
+ import pc from 'picocolors';
12
+ // Colored output helpers
13
+ const log = {
14
+ error: (msg) => console.log(`${pc.red('✗')} ${msg}`),
15
+ success: (msg) => console.log(`${pc.green('✓')} ${msg}`),
16
+ info: (msg) => console.log(`${pc.blue('ℹ')} ${msg}`),
17
+ warn: (msg) => console.log(`${pc.yellow('⚠')} ${msg}`),
18
+ step: (msg) => console.log(`${pc.cyan('→')} ${msg}`),
19
+ dim: (msg) => console.log(pc.dim(msg)),
20
+ plain: (msg) => console.log(msg),
21
+ header: (msg) => console.log(`\n${pc.bold(msg)}`),
22
+ cmd: (msg) => console.log(` ${pc.dim('$')} ${pc.cyan(msg)}`),
23
+ label: (label, value) => console.log(` ${pc.dim(label)} ${value}`),
24
+ };
11
25
  // Read version from package.json
12
26
  const __filename = fileURLToPath(import.meta.url);
13
27
  const __dirname = path.dirname(__filename);
@@ -144,20 +158,20 @@ async function checkDependencies() {
144
158
  }
145
159
  }
146
160
  if (missingDeps.length > 0) {
147
- console.log('Missing required dependencies:');
148
- console.log(`\n🖥️ Detected OS: ${osInfo.platform} ${osInfo.arch}${osInfo.isWSL ? ' (WSL)' : ''}`);
161
+ log.error('Missing required dependencies');
162
+ log.plain(`\n${pc.dim('Detected OS:')} ${osInfo.platform} ${osInfo.arch}${osInfo.isWSL ? ' (WSL)' : ''}`);
149
163
  for (const dep of missingDeps) {
150
- console.log(`\n📦 ${dep.name} (required for ${dep.requiredFor})`);
164
+ log.plain(`\n${pc.yellow(dep.name)} ${pc.dim(`(required for ${dep.requiredFor})`)}`);
151
165
  const instructions = getInstallInstructions(dep.name, osInfo);
152
166
  if (instructions.length > 0) {
153
- console.log(' Installation options:');
154
- instructions.forEach(instruction => { console.log(instruction); });
167
+ log.plain(pc.dim(' Installation options:'));
168
+ instructions.forEach(instruction => { log.plain(instruction); });
155
169
  }
156
170
  else {
157
- console.log(` General instructions: ${dep.installMsg}`);
171
+ log.plain(` ${pc.dim('Instructions:')} ${dep.installMsg}`);
158
172
  }
159
173
  }
160
- console.log('\n💡 After installing the dependencies, run Harbor again.');
174
+ log.plain(`\n${pc.dim('After installing dependencies, run Harbor again.')}`);
161
175
  throw new Error('Please install missing dependencies before continuing');
162
176
  }
163
177
  }
@@ -167,11 +181,11 @@ function promptConfigLocation() {
167
181
  output: process.stdout,
168
182
  });
169
183
  return new Promise((resolve) => {
170
- console.log('\nFound package.json. Where would you like to store harbor config?');
171
- console.log(' 1. package.json (keeps everything in one place)');
172
- console.log(' 2. harbor.json (separate config file, auto-IntelliSense)\n');
184
+ log.plain(`\n${pc.bold('Found package.json.')} Where would you like to store harbor config?`);
185
+ log.plain(` ${pc.cyan('1.')} package.json ${pc.dim('(keeps everything in one place)')}`);
186
+ log.plain(` ${pc.cyan('2.')} harbor.json ${pc.dim('(separate config file, auto-IntelliSense)')}\n`);
173
187
  const ask = () => {
174
- rl.question('Enter choice (1 or 2): ', (answer) => {
188
+ rl.question(`Enter choice ${pc.dim('(1 or 2)')}: `, (answer) => {
175
189
  const choice = answer.trim();
176
190
  if (choice === '1') {
177
191
  rl.close();
@@ -182,7 +196,7 @@ function promptConfigLocation() {
182
196
  resolve('harbor.json');
183
197
  }
184
198
  else {
185
- console.log('Please enter 1 or 2');
199
+ log.warn('Please enter 1 or 2');
186
200
  ask();
187
201
  }
188
202
  });
@@ -201,192 +215,173 @@ const possibleProjectFiles = [
201
215
  'build.gradle', // Java Gradle projects
202
216
  ];
203
217
  const program = new Command();
204
- program
205
- .name('harbor')
206
- .description(`A CLI tool for orchestrating multiple local development services in tmux.
218
+ // Custom help formatting
219
+ function showCustomHelp() {
220
+ const dim = pc.dim;
221
+ const bold = pc.bold;
222
+ const cyan = pc.cyan;
223
+ const yellow = pc.yellow;
224
+ const green = pc.green;
225
+ console.log(`
226
+ ${bold('⚓ Harbor')} ${dim(`v${packageJson.version}`)}
227
+ ${dim('Orchestrate local dev services in tmux')}
207
228
 
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.
229
+ ${yellow('Usage:')}
230
+ ${dim('$')} harbor ${cyan('<command>')} ${dim('[options]')}
211
231
 
212
- REQUIREMENTS:
213
- - tmux (terminal multiplexer)
214
- - jq (JSON processor)
232
+ ${yellow('Commands:')}
233
+ ${green('dock')} Scan directories and create harbor.json config
234
+ ${green('moor')} Add new services to existing config
235
+ ${green('launch')} Start all services in tmux ${dim('(-d for headless)')}
236
+ ${green('anchor')} Attach to a running Harbor session
237
+ ${green('scuttle')} Stop all services
238
+ ${green('bearings')} Show status of running services
215
239
 
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
240
+ ${yellow('Quick Start:')}
241
+ ${dim('$')} harbor dock ${dim('# Create config')}
242
+ ${dim('$')} harbor launch ${dim('# Start services')}
243
+ ${dim('$')} harbor launch -d ${dim('# Start headless')}
223
244
 
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.
245
+ ${yellow('Options:')}
246
+ ${cyan('-V, --version')} Show version
247
+ ${cyan('-h, --help')} Show help for command
227
248
 
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`)
249
+ ${dim('Run')} harbor ${cyan('<command>')} --help ${dim('for detailed command info')}
250
+ `);
251
+ }
252
+ program
253
+ .name('harbor')
254
+ .description('Orchestrate local dev services in tmux')
235
255
  .version(packageJson.version)
236
256
  .action(async () => await checkDependencies())
237
- .addHelpCommand(false);
238
- // If no command is provided, display help
257
+ .addHelpCommand(false)
258
+ .configureHelp({
259
+ sortSubcommands: false,
260
+ sortOptions: false,
261
+ });
262
+ // Override help display
263
+ program.helpInformation = () => '';
264
+ program.on('--help', () => { });
265
+ // If no command is provided, display custom help
239
266
  if (process.argv.length <= 2) {
240
- program.help();
267
+ showCustomHelp();
268
+ process.exit(0);
269
+ }
270
+ // Handle -h and --help for main command
271
+ if (process.argv.includes('-h') || process.argv.includes('--help')) {
272
+ if (process.argv.length === 3) {
273
+ showCustomHelp();
274
+ process.exit(0);
275
+ }
241
276
  }
242
277
  program.command('dock')
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)', './')
278
+ .description('Scan directories and create harbor.json config')
279
+ .option('-p, --path <path>', 'Directory to scan for service projects', './')
258
280
  .action(async (options) => {
259
281
  try {
260
282
  await checkDependencies();
261
283
  const configExists = checkHasHarborConfig();
262
284
  if (configExists) {
263
- console.log('❌ Error: Harbor project already initialized');
264
- console.log(' - Harbor configuration already exists');
265
- console.log('\nTo reinitialize, please remove the configuration first.');
285
+ log.error('Harbor project already initialized');
286
+ log.dim(' Configuration already exists');
287
+ log.plain('');
288
+ log.info('To reinitialize, remove the existing configuration first.');
266
289
  process.exit(1);
267
290
  }
268
291
  await generateDevFile(options.path);
269
- console.log('✨ Environment prepared!');
292
+ log.plain('');
293
+ log.success(pc.green('Environment prepared!'));
270
294
  }
271
295
  catch (err) {
272
- console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
296
+ log.error(err instanceof Error ? err.message : 'Unknown error');
273
297
  process.exit(1);
274
298
  }
275
299
  });
276
300
  program.command('moor')
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`)
301
+ .description('Add new services to existing config')
291
302
  .option('-p, --path <path>', 'Directory to scan for new service projects', './')
292
303
  .action(async (options) => {
293
304
  try {
294
305
  await checkDependencies();
295
306
  if (!checkHasHarborConfig()) {
296
- console.log('No harbor configuration found');
297
- console.log('\nTo initialize a new Harbor project, please use:');
298
- console.log(' harbor dock');
307
+ log.error('No harbor configuration found');
308
+ log.plain('');
309
+ log.info('To initialize a new Harbor project:');
310
+ log.cmd('harbor dock');
299
311
  process.exit(1);
300
312
  }
301
313
  await generateDevFile(options.path);
302
314
  }
303
315
  catch (err) {
304
- console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
316
+ log.error(err instanceof Error ? err.message : 'Unknown error');
305
317
  process.exit(1);
306
318
  }
307
319
  });
308
320
  program.command('launch')
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.')
321
+ .description('Start all services in tmux (-d for headless)')
322
+ .option('-d, --detach', 'Run in background (headless mode)')
333
323
  .option('--headless', 'Alias for --detach')
334
- .option('--name <name>', 'Override tmux session name from config')
324
+ .option('--name <name>', 'Override tmux session name')
335
325
  .action(async (options) => {
336
326
  try {
327
+ const isDetached = options.detach || options.headless;
328
+ // Check if already inside a tmux session (only matters for attached mode)
329
+ if (!isDetached && process.env.TMUX) {
330
+ log.error('Cannot launch in attached mode from inside a tmux session');
331
+ log.plain('');
332
+ log.info('Options:');
333
+ log.plain(` ${pc.cyan('1.')} Use headless mode: ${pc.cyan('harbor launch -d')}`);
334
+ log.plain(` ${pc.cyan('2.')} Detach from current session ${pc.dim('(Ctrl+b then d)')} and try again`);
335
+ process.exit(1);
336
+ }
337
337
  await checkDependencies();
338
- await runServices({ detach: options.detach || options.headless, name: options.name });
338
+ await runServices({ detach: isDetached, name: options.name });
339
339
  }
340
340
  catch (err) {
341
- console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
341
+ log.error(err instanceof Error ? err.message : 'Unknown error');
342
342
  process.exit(1);
343
343
  }
344
344
  });
345
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
- - Runs any configured 'after' scripts if the session is killed
356
-
357
- PREREQUISITES: Services must be running (started with 'harbor launch').
358
-
359
- EXAMPLES:
360
- harbor launch -d # Start in background
361
- harbor anchor # Attach to default session
362
- harbor anchor --name my-app # Attach to a specific named session`)
363
- .option('--name <name>', 'Specify which tmux session to attach to (defaults to config sessionName or "harbor")')
346
+ .description('Attach to a running Harbor session')
347
+ .option('--name <name>', 'Session name to attach to')
364
348
  .action(async (options) => {
365
349
  try {
350
+ // Check if already inside a tmux session
351
+ if (process.env.TMUX) {
352
+ log.error('Cannot anchor from inside a tmux session');
353
+ log.plain('');
354
+ log.info('You are already inside a tmux session. To attach:');
355
+ log.plain(` ${pc.cyan('1.')} Detach from current session ${pc.dim('(Ctrl+b then d)')}`);
356
+ log.plain(` ${pc.cyan('2.')} Run ${pc.cyan('harbor anchor')} from a regular terminal`);
357
+ process.exit(1);
358
+ }
366
359
  const config = await readHarborConfig();
367
360
  const sessionName = options.name || config.sessionName || 'harbor';
361
+ const socketName = `harbor-${sessionName}`;
368
362
  // Check if session exists
369
- const checkSession = spawn('tmux', ['has-session', '-t', sessionName], {
363
+ const checkSession = spawn('tmux', ['-L', socketName, 'has-session', '-t', sessionName], {
370
364
  stdio: 'pipe',
371
365
  });
372
366
  await new Promise((resolve) => {
373
367
  checkSession.on('close', (code) => {
374
368
  if (code !== 0) {
375
- console.log(`❌ No running Harbor session found (looking for: ${sessionName})`);
376
- console.log('\nTo start services, run:');
377
- console.log(' harbor launch');
369
+ log.error(`No running Harbor session found ${pc.dim(`(looking for: ${sessionName})`)}`);
370
+ log.plain('');
371
+ log.info('To start services:');
372
+ log.cmd('harbor launch');
378
373
  process.exit(1);
379
374
  }
380
375
  resolve();
381
376
  });
382
377
  });
383
378
  // Attach to the session
384
- const attach = spawn('tmux', ['attach-session', '-t', sessionName], {
379
+ const attach = spawn('tmux', ['-L', socketName, 'attach-session', '-t', sessionName], {
385
380
  stdio: 'inherit',
386
381
  });
387
382
  attach.on('close', async (code) => {
388
383
  // Check if session was killed (vs just detached)
389
- const checkAfter = spawn('tmux', ['has-session', '-t', sessionName], {
384
+ const checkAfter = spawn('tmux', ['-L', socketName, 'has-session', '-t', sessionName], {
390
385
  stdio: 'pipe',
391
386
  });
392
387
  const sessionStillExists = await new Promise((resolve) => {
@@ -400,7 +395,7 @@ EXAMPLES:
400
395
  await execute(config.after, 'after');
401
396
  }
402
397
  catch {
403
- console.error('After scripts failed');
398
+ log.error('After scripts failed');
404
399
  process.exit(1);
405
400
  }
406
401
  }
@@ -408,33 +403,20 @@ EXAMPLES:
408
403
  });
409
404
  }
410
405
  catch (err) {
411
- console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
406
+ log.error(err instanceof Error ? err.message : 'Unknown error');
412
407
  process.exit(1);
413
408
  }
414
409
  });
415
410
  program.command('scuttle')
416
- .description(`Stop all running Harbor services by killing the tmux session.
417
-
418
- WHEN TO USE: When you want to stop all services and free up resources.
419
-
420
- WHAT IT DOES:
421
- - Finds the running Harbor tmux session
422
- - Kills the entire session (all service windows)
423
- - All services stop immediately
424
- - Runs any configured 'after' scripts
425
-
426
- SAFE TO RUN: If no session is running, it simply reports that and exits cleanly.
427
-
428
- EXAMPLES:
429
- harbor scuttle # Stop default session
430
- harbor scuttle --name my-app # Stop a specific named session`)
431
- .option('--name <name>', 'Specify which tmux session to stop (defaults to config sessionName or "harbor")')
411
+ .description('Stop all services and kill the session')
412
+ .option('--name <name>', 'Session name to stop')
432
413
  .action(async (options) => {
433
414
  try {
434
415
  const config = await readHarborConfig();
435
416
  const sessionName = options.name || config.sessionName || 'harbor';
417
+ const socketName = `harbor-${sessionName}`;
436
418
  // Check if session exists
437
- const checkSession = spawn('tmux', ['has-session', '-t', sessionName], {
419
+ const checkSession = spawn('tmux', ['-L', socketName, 'has-session', '-t', sessionName], {
438
420
  stdio: 'pipe',
439
421
  });
440
422
  const sessionExists = await new Promise((resolve) => {
@@ -443,68 +425,48 @@ EXAMPLES:
443
425
  });
444
426
  });
445
427
  if (!sessionExists) {
446
- console.log(`ℹ️ No running Harbor session found (looking for: ${sessionName})`);
428
+ log.info(`No running Harbor session found ${pc.dim(`(looking for: ${sessionName})`)}`);
447
429
  process.exit(0);
448
430
  }
449
431
  // Kill the session
450
- const killSession = spawn('tmux', ['kill-session', '-t', sessionName], {
432
+ const killSession = spawn('tmux', ['-L', socketName, 'kill-session', '-t', sessionName], {
451
433
  stdio: 'inherit',
452
434
  });
453
435
  killSession.on('close', async (code) => {
454
436
  if (code === 0) {
455
- console.log(`✅ Harbor session '${sessionName}' stopped`);
437
+ log.success(`Harbor session ${pc.cyan(sessionName)} stopped`);
456
438
  // Execute after scripts when session is killed
457
439
  if (config.after && config.after.length > 0) {
458
440
  try {
459
441
  await execute(config.after, 'after');
460
442
  }
461
443
  catch {
462
- console.error('After scripts failed');
444
+ log.error('After scripts failed');
463
445
  process.exit(1);
464
446
  }
465
447
  }
466
448
  }
467
449
  else {
468
- console.log('Failed to stop Harbor session');
450
+ log.error('Failed to stop Harbor session');
469
451
  }
470
452
  process.exit(code || 0);
471
453
  });
472
454
  }
473
455
  catch (err) {
474
- console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
456
+ log.error(err instanceof Error ? err.message : 'Unknown error');
475
457
  process.exit(1);
476
458
  }
477
459
  });
478
460
  program.command('bearings')
479
- .description(`Show status of running Harbor services and session information.
480
-
481
- WHEN TO USE: To check if services are running, especially in headless mode.
482
-
483
- WHAT IT SHOWS:
484
- - Session name and running status
485
- - List of service windows (with 📄 indicator if logging enabled)
486
- - Log file paths and sizes
487
- - Available commands (anchor, scuttle)
488
-
489
- OUTPUT EXAMPLE:
490
- ⚓ Harbor Status
491
- Session: harbor
492
- Status: Running ✓
493
- Services: [0] Terminal, [1] web 📄, [2] api 📄
494
- Logs: .harbor/harbor-web.log (1.2 KB)
495
-
496
- SAFE TO RUN: Works whether services are running or not.
497
-
498
- EXAMPLES:
499
- harbor bearings # Check default session
500
- harbor bearings --name my-app # Check a specific named session`)
501
- .option('--name <name>', 'Specify which tmux session to check (defaults to config sessionName or "harbor")')
461
+ .description('Show status of running services')
462
+ .option('--name <name>', 'Session name to check')
502
463
  .action(async (options) => {
503
464
  try {
504
465
  const config = await readHarborConfig();
505
466
  const sessionName = options.name || config.sessionName || 'harbor';
467
+ const socketName = `harbor-${sessionName}`;
506
468
  // Check if session exists
507
- const checkSession = spawn('tmux', ['has-session', '-t', sessionName], {
469
+ const checkSession = spawn('tmux', ['-L', socketName, 'has-session', '-t', sessionName], {
508
470
  stdio: 'pipe',
509
471
  });
510
472
  const sessionExists = await new Promise((resolve) => {
@@ -513,16 +475,19 @@ EXAMPLES:
513
475
  });
514
476
  });
515
477
  if (!sessionExists) {
516
- console.log(`\n⚓ Harbor Status\n`);
517
- console.log(` Session: ${sessionName}`);
518
- console.log(` Status: Not running\n`);
519
- console.log(` To start services, run:`);
520
- console.log(` harbor launch # interactive mode`);
521
- console.log(` harbor launch -d # headless mode\n`);
478
+ log.header(`${pc.cyan('')} Harbor Status`);
479
+ log.plain('');
480
+ log.label('Session:', sessionName);
481
+ log.label('Status:', pc.yellow('Not running'));
482
+ log.plain('');
483
+ log.info('To start services:');
484
+ log.cmd(`harbor launch ${pc.dim('# interactive mode')}`);
485
+ log.cmd(`harbor launch -d ${pc.dim('# headless mode')}`);
486
+ log.plain('');
522
487
  process.exit(0);
523
488
  }
524
489
  // Get list of windows (services)
525
- const listWindows = spawn('tmux', ['list-windows', '-t', sessionName, '-F', '#{window_index}|#{window_name}|#{pane_current_command}'], {
490
+ const listWindows = spawn('tmux', ['-L', socketName, 'list-windows', '-t', sessionName, '-F', '#{window_index}|#{window_name}|#{pane_current_command}'], {
526
491
  stdio: ['pipe', 'pipe', 'pipe'],
527
492
  });
528
493
  let windowOutput = '';
@@ -533,38 +498,43 @@ EXAMPLES:
533
498
  listWindows.on('close', () => resolve());
534
499
  });
535
500
  const windows = windowOutput.trim().split('\n').filter(Boolean);
536
- console.log(`\n⚓ Harbor Status\n`);
537
- console.log(` Session: ${sessionName}`);
538
- console.log(` Status: Running ✓`);
539
- console.log(` Windows: ${windows.length}\n`);
540
- console.log(` Services:`);
501
+ log.header(`${pc.cyan('')} Harbor Status`);
502
+ log.plain('');
503
+ log.label('Session:', sessionName);
504
+ log.label('Status:', pc.green('Running ✓'));
505
+ log.label('Windows:', String(windows.length));
506
+ log.plain('');
507
+ log.plain(` ${pc.dim('Services:')}`);
541
508
  for (const window of windows) {
542
- const [index, name, cmd] = window.split('|');
509
+ const [index, name] = window.split('|');
543
510
  const logFile = `.harbor/${sessionName}-${name}.log`;
544
511
  const hasLog = fs.existsSync(path.join(process.cwd(), logFile));
545
- const logIndicator = hasLog ? ` 📄` : '';
546
- console.log(` [${index}] ${name}${logIndicator}`);
512
+ const logIndicator = hasLog ? pc.dim(' 📄') : '';
513
+ log.plain(` ${pc.dim(`[${index}]`)} ${pc.cyan(name)}${logIndicator}`);
547
514
  }
548
515
  // Check for log files
549
516
  const harborDir = path.join(process.cwd(), '.harbor');
550
517
  if (fs.existsSync(harborDir)) {
551
518
  const logFiles = fs.readdirSync(harborDir).filter(f => f.endsWith('.log'));
552
519
  if (logFiles.length > 0) {
553
- console.log(`\n Logs:`);
520
+ log.plain('');
521
+ log.plain(` ${pc.dim('Logs:')}`);
554
522
  for (const logFile of logFiles) {
555
523
  const logPath = path.join(harborDir, logFile);
556
524
  const stats = fs.statSync(logPath);
557
525
  const sizeKB = (stats.size / 1024).toFixed(1);
558
- console.log(` .harbor/${logFile} (${sizeKB} KB)`);
526
+ log.plain(` ${pc.dim(`.harbor/${logFile}`)} ${pc.dim(`(${sizeKB} KB)`)}`);
559
527
  }
560
528
  }
561
529
  }
562
- console.log(`\n Commands:`);
563
- console.log(` harbor anchor - Anchor to the session`);
564
- console.log(` harbor scuttle - Stop all services\n`);
530
+ log.plain('');
531
+ log.plain(` ${pc.dim('Commands:')}`);
532
+ log.plain(` ${pc.cyan('harbor anchor')} ${pc.dim('Attach to the session')}`);
533
+ log.plain(` ${pc.cyan('harbor scuttle')} ${pc.dim('Stop all services')}`);
534
+ log.plain('');
565
535
  }
566
536
  catch (err) {
567
- console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
537
+ log.error(err instanceof Error ? err.message : 'Unknown error');
568
538
  process.exit(1);
569
539
  }
570
540
  });
@@ -634,7 +604,7 @@ async function generateDevFile(dirPath) {
634
604
  try {
635
605
  const existing = await fs.promises.readFile('harbor.json', 'utf-8');
636
606
  config = JSON.parse(existing);
637
- console.log('Found existing harbor.json, scanning for new services...');
607
+ log.info('Found existing harbor.json, scanning for new services...');
638
608
  }
639
609
  catch (err) {
640
610
  if (err.code !== 'ENOENT') {
@@ -648,7 +618,7 @@ async function generateDevFile(dirPath) {
648
618
  // Existing harbor config in package.json, use it
649
619
  config = packageJson.harbor;
650
620
  writeToPackageJson = true;
651
- console.log('Found existing harbor config in package.json, scanning for new services...');
621
+ log.info('Found existing harbor config in package.json, scanning for new services...');
652
622
  }
653
623
  else {
654
624
  // package.json exists but no harbor config - ask user where to store it
@@ -659,7 +629,7 @@ async function generateDevFile(dirPath) {
659
629
  before: [],
660
630
  after: [],
661
631
  };
662
- console.log(`Creating new harbor config in ${choice}...`);
632
+ log.step(`Creating new harbor config in ${pc.cyan(choice)}...`);
663
633
  }
664
634
  }
665
635
  catch (err) {
@@ -670,7 +640,7 @@ async function generateDevFile(dirPath) {
670
640
  config = {
671
641
  services: [],
672
642
  };
673
- console.log('Creating new harbor.json...');
643
+ log.step(`Creating new ${pc.cyan('harbor.json')}...`);
674
644
  }
675
645
  }
676
646
  // Create a map of existing services for easy lookup
@@ -694,19 +664,19 @@ async function generateDevFile(dirPath) {
694
664
  service.command = 'go run .';
695
665
  }
696
666
  config.services.push(service);
697
- console.log(`Added new service: ${folder.name}`);
667
+ log.success(`Added service: ${pc.green(folder.name)}`);
698
668
  newServicesAdded = true;
699
669
  }
700
670
  else if (existing.has(folder.name)) {
701
- console.log(`Skipping existing service: ${folder.name}`);
671
+ log.dim(` Skipping existing service: ${folder.name}`);
702
672
  }
703
673
  else {
704
- console.log(`Skipping directory ${folder.name} (no recognized project files)`);
674
+ log.dim(` Skipping directory ${folder.name} (no recognized project files)`);
705
675
  }
706
676
  }
707
677
  }
708
678
  if (!newServicesAdded) {
709
- console.log('No new services found to add, feel free to add them manually');
679
+ log.info('No new services found to add, feel free to add them manually');
710
680
  }
711
681
  const validationError = validateConfig(config);
712
682
  if (validationError) {
@@ -718,15 +688,15 @@ async function generateDevFile(dirPath) {
718
688
  const packageJson = JSON.parse(packageData);
719
689
  packageJson.harbor = config;
720
690
  await fs.promises.writeFile('package.json', JSON.stringify(packageJson, null, 2), 'utf-8');
721
- console.log('\npackage.json updated successfully with harbor configuration');
722
- console.log('\n💡 Tip: To enable IntelliSense for the harbor config in package.json,');
723
- console.log(' add this to your .vscode/settings.json:');
724
- console.log(' {');
725
- console.log(' "json.schemas": [{');
726
- console.log(' "fileMatch": ["package.json"],');
727
- console.log(' "url": "https://raw.githubusercontent.com/Abyrd9/harbor-cli/main/harbor.package-json.schema.json"');
728
- console.log(' }]');
729
- console.log(' }');
691
+ log.success(`${pc.cyan('package.json')} updated with harbor configuration`);
692
+ log.plain('');
693
+ log.info(`${pc.dim('Tip:')} To enable IntelliSense, add this to ${pc.cyan('.vscode/settings.json')}:`);
694
+ log.plain(pc.dim(' {'));
695
+ log.plain(pc.dim(' "json.schemas": [{'));
696
+ log.plain(pc.dim(' "fileMatch": ["package.json"],'));
697
+ log.plain(pc.dim(' "url": "https://raw.githubusercontent.com/Abyrd9/harbor-cli/main/harbor.package-json.schema.json"'));
698
+ log.plain(pc.dim(' }]'));
699
+ log.plain(pc.dim(' }'));
730
700
  }
731
701
  else {
732
702
  // Write to harbor.json with $schema for IntelliSense
@@ -735,10 +705,10 @@ async function generateDevFile(dirPath) {
735
705
  ...config,
736
706
  };
737
707
  await fs.promises.writeFile('harbor.json', JSON.stringify(configWithSchema, null, 2), 'utf-8');
738
- console.log('\nharbor.json created successfully');
708
+ log.success(`${pc.cyan('harbor.json')} created successfully`);
739
709
  }
740
- console.log('\nImportant:');
741
- console.log(' - Verify the auto-detected commands are correct for your services');
710
+ log.plain('');
711
+ log.info(`${pc.dim('Important:')} Verify the auto-detected commands are correct for your services`);
742
712
  return true;
743
713
  }
744
714
  catch (err) {
@@ -785,11 +755,12 @@ async function execute(scripts, scriptType) {
785
755
  if (!scripts || scripts.length === 0) {
786
756
  return;
787
757
  }
788
- console.log(`\n🚀 Executing ${scriptType} scripts...`);
758
+ log.header(`Running ${scriptType} scripts...`);
789
759
  for (let i = 0; i < scripts.length; i++) {
790
760
  const script = scripts[i];
791
- console.log(`\n📋 Running ${scriptType} script ${i + 1}/${scripts.length}: ${script.command}`);
792
- console.log(` 📁 In directory: ${script.path}`);
761
+ log.plain('');
762
+ log.step(`${pc.dim(`[${i + 1}/${scripts.length}]`)} ${pc.cyan(script.command)}`);
763
+ log.dim(` in ${script.path}`);
793
764
  try {
794
765
  await new Promise((resolve, reject) => {
795
766
  const process = spawn('sh', ['-c', `cd "${script.path}" && ${script.command}`], {
@@ -797,7 +768,7 @@ async function execute(scripts, scriptType) {
797
768
  });
798
769
  process.on('close', (code) => {
799
770
  if (code === 0) {
800
- console.log(`✅ ${scriptType} script ${i + 1} completed successfully`);
771
+ log.success(`${scriptType} script ${i + 1} completed`);
801
772
  resolve(null);
802
773
  }
803
774
  else {
@@ -810,18 +781,20 @@ async function execute(scripts, scriptType) {
810
781
  });
811
782
  }
812
783
  catch (err) {
813
- console.error(`❌ Error executing ${scriptType} script ${i + 1}:`, err instanceof Error ? err.message : 'Unknown error');
784
+ log.error(`Error executing ${scriptType} script ${i + 1}: ${err instanceof Error ? err.message : 'Unknown error'}`);
814
785
  throw err;
815
786
  }
816
787
  }
817
- console.log(`\n✅ All ${scriptType} scripts completed successfully`);
788
+ log.plain('');
789
+ log.success(`All ${scriptType} scripts completed`);
818
790
  }
819
791
  async function runServices(options = {}) {
820
792
  const hasHarborConfig = checkHasHarborConfig();
821
793
  if (!hasHarborConfig) {
822
- console.log('No harbor configuration found');
823
- console.log('\nTo initialize a new Harbor project, please use:');
824
- console.log(' harbor dock');
794
+ log.error('No harbor configuration found');
795
+ log.plain('');
796
+ log.info('To initialize a new Harbor project:');
797
+ log.cmd('harbor dock');
825
798
  process.exit(1);
826
799
  }
827
800
  // Load and validate config
@@ -830,12 +803,12 @@ async function runServices(options = {}) {
830
803
  config = await readHarborConfig();
831
804
  const validationError = validateConfig(config);
832
805
  if (validationError) {
833
- console.log(`❌ Invalid harbor.json configuration: ${validationError}`);
806
+ log.error(`Invalid harbor.json configuration: ${validationError}`);
834
807
  process.exit(1);
835
808
  }
836
809
  }
837
810
  catch (err) {
838
- console.error('Error reading config:', err);
811
+ log.error(`Error reading config: ${err}`);
839
812
  process.exit(1);
840
813
  }
841
814
  ensureLogSetup(config);
@@ -844,7 +817,7 @@ async function runServices(options = {}) {
844
817
  await execute(config.before || [], 'before');
845
818
  }
846
819
  catch {
847
- console.error('Before scripts failed, aborting launch');
820
+ log.error('Before scripts failed, aborting launch');
848
821
  process.exit(1);
849
822
  }
850
823
  // Ensure scripts exist and are executable
@@ -862,12 +835,12 @@ async function runServices(options = {}) {
862
835
  });
863
836
  return new Promise((resolve) => {
864
837
  command.on('error', (err) => {
865
- console.error(`Error running dev.sh: ${err}`);
838
+ log.error(`Error running dev.sh: ${err}`);
866
839
  process.exit(1);
867
840
  });
868
841
  command.on('close', async (code) => {
869
842
  if (code !== 0) {
870
- console.error(`dev.sh exited with code ${code}`);
843
+ log.error(`dev.sh exited with code ${code}`);
871
844
  process.exit(1);
872
845
  }
873
846
  // Only execute after scripts in attached mode
@@ -877,7 +850,7 @@ async function runServices(options = {}) {
877
850
  await execute(config.after || [], 'after');
878
851
  }
879
852
  catch {
880
- console.error('After scripts failed');
853
+ log.error('After scripts failed');
881
854
  process.exit(1);
882
855
  }
883
856
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abyrd9/harbor-cli",
3
- "version": "2.3.2",
3
+ "version": "2.3.3",
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": {
@@ -42,7 +42,8 @@
42
42
  "node": ">=18.0.0"
43
43
  },
44
44
  "dependencies": {
45
- "@commander-js/extra-typings": "^14.0.0"
45
+ "@commander-js/extra-typings": "^14.0.0",
46
+ "picocolors": "^1.1.1"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@types/node": "^24.0.3",
package/scripts/dev.sh CHANGED
@@ -14,10 +14,15 @@ get_harbor_config() {
14
14
  # Get session name from env, config, or use default
15
15
  session_name="${HARBOR_SESSION_NAME:-$(get_harbor_config | jq -r '.sessionName // "harbor"')}"
16
16
 
17
+ # Use a separate tmux socket for Harbor to avoid conflicts with existing tmux sessions
18
+ # This prevents Harbor's global options from affecting other tmux sessions
19
+ socket_name="harbor-${session_name}"
20
+ tmux_cmd="tmux -L $socket_name"
21
+
17
22
  # Check if the session already exists and kill it
18
- if tmux has-session -t "$session_name" 2>/dev/null; then
23
+ if $tmux_cmd has-session -t "$session_name" 2>/dev/null; then
19
24
  echo "Killing existing tmux session '$session_name'"
20
- tmux kill-session -t "$session_name"
25
+ $tmux_cmd kill-session -t "$session_name"
21
26
  fi
22
27
  repo_root="$(pwd)"
23
28
  max_log_lines=1000
@@ -51,70 +56,70 @@ start_log_trim() {
51
56
  trap cleanup_logs EXIT
52
57
 
53
58
  # Start a new tmux session and rename the initial window
54
- tmux new-session -d -s "$session_name"
59
+ $tmux_cmd new-session -d -s "$session_name"
55
60
 
56
61
  # Set tmux options
57
- tmux set-option -g prefix C-a
58
- tmux bind-key C-a send-prefix
59
- tmux set-option -g mouse on
60
- tmux set-option -g history-limit 50000
61
- tmux set-window-option -g mode-keys vi
62
+ $tmux_cmd set-option -g prefix C-a
63
+ $tmux_cmd bind-key C-a send-prefix
64
+ $tmux_cmd set-option -g mouse on
65
+ $tmux_cmd set-option -g history-limit 50000
66
+ $tmux_cmd set-window-option -g mode-keys vi
62
67
 
63
68
  # 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
69
+ $tmux_cmd set-option -g extended-keys on
70
+ $tmux_cmd set-option -g xterm-keys on
66
71
 
67
72
  # Add binding to kill session with Ctrl+q
68
- tmux bind-key -n C-q kill-session
73
+ $tmux_cmd bind-key -n C-q kill-session
69
74
 
70
75
  # Add padding and styling to panes
71
- tmux set-option -g pane-border-style fg="#3f3f3f"
72
- tmux set-option -g pane-active-border-style fg="#6366f1"
73
- tmux set-option -g pane-border-status top
74
- tmux set-option -g pane-border-format ""
76
+ $tmux_cmd set-option -g pane-border-style fg="#3f3f3f"
77
+ $tmux_cmd set-option -g pane-active-border-style fg="#6366f1"
78
+ $tmux_cmd set-option -g pane-border-status top
79
+ $tmux_cmd set-option -g pane-border-format ""
75
80
 
76
81
  # Add padding inside panes
77
- tmux set-option -g status-left-length 100
78
- tmux set-option -g status-right-length 100
79
- tmux set-window-option -g window-style 'fg=colour247,bg=colour236'
80
- tmux set-window-option -g window-active-style 'fg=colour250,bg=black'
82
+ $tmux_cmd set-option -g status-left-length 100
83
+ $tmux_cmd set-option -g status-right-length 100
84
+ $tmux_cmd set-window-option -g window-style 'fg=colour247,bg=colour236'
85
+ $tmux_cmd set-window-option -g window-active-style 'fg=colour250,bg=black'
81
86
 
82
87
  # Set inner padding
83
- tmux set-option -g window-style "bg=#1c1917 fg=#a8a29e"
84
- tmux set-option -g window-active-style "bg=#1c1917 fg=#ffffff"
88
+ $tmux_cmd set-option -g window-style "bg=#1c1917 fg=#a8a29e"
89
+ $tmux_cmd set-option -g window-active-style "bg=#1c1917 fg=#ffffff"
85
90
 
86
91
  # Improve copy mode and mouse behavior
87
- tmux set-option -g set-clipboard external
88
- tmux bind-key -T copy-mode-vi v send-keys -X begin-selection
89
- tmux bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel
90
- tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"
92
+ $tmux_cmd set-option -g set-clipboard external
93
+ $tmux_cmd bind-key -T copy-mode-vi v send-keys -X begin-selection
94
+ $tmux_cmd bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel
95
+ $tmux_cmd bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"
91
96
 
92
97
  # Set easier window navigation shortcuts (Shift+Left/Right to switch windows)
93
- tmux bind-key -n S-Left select-window -t :-
94
- tmux bind-key -n S-Right select-window -t :+
98
+ $tmux_cmd bind-key -n S-Left select-window -t :-
99
+ $tmux_cmd bind-key -n S-Right select-window -t :+
95
100
 
96
101
  # Configure status bar
97
- tmux set-option -g status-position top
98
- tmux set-option -g status-style bg="#1c1917",fg="#a8a29e"
99
- tmux set-option -g status-left ""
100
- tmux set-option -g status-right "#[fg=#a8a29e]shift+←/→ switch · ctrl+q close · #[fg=white]%H:%M#[default]"
101
- tmux set-window-option -g window-status-current-format "\
102
+ $tmux_cmd set-option -g status-position top
103
+ $tmux_cmd set-option -g status-style bg="#1c1917",fg="#a8a29e"
104
+ $tmux_cmd set-option -g status-left ""
105
+ $tmux_cmd set-option -g status-right "#[fg=#a8a29e]shift+←/→ switch · ctrl+q close · #[fg=white]%H:%M#[default]"
106
+ $tmux_cmd set-window-option -g window-status-current-format "\
102
107
  #[fg=#6366f1, bg=#1c1917] →\
103
108
  #[fg=#6366f1, bg=#1c1917, bold] #W\
104
109
  #[fg=#6366f1, bg=#1c1917] "
105
- tmux set-window-option -g window-status-format "\
110
+ $tmux_cmd set-window-option -g window-status-format "\
106
111
  #[fg=#a8a29e, bg=#1c1917] \
107
112
  #[fg=#a8a29e, bg=#1c1917] #W \
108
113
  #[fg=#a8a29e, bg=#1c1917] "
109
114
 
110
115
  # Add padding below status bar
111
- tmux set-option -g status 2
112
- tmux set-option -Fg 'status-format[1]' '#{status-format[0]}'
113
- tmux set-option -g 'status-format[0]' ''
116
+ $tmux_cmd set-option -g status 2
117
+ $tmux_cmd set-option -Fg 'status-format[1]' '#{status-format[0]}'
118
+ $tmux_cmd set-option -g 'status-format[0]' ''
114
119
 
115
120
  # Create a new window for the interactive shell
116
121
  echo "Creating window for interactive shell"
117
- tmux rename-window -t "$session_name":0 'Terminal'
122
+ $tmux_cmd rename-window -t "$session_name":0 'Terminal'
118
123
 
119
124
  window_index=1 # Start from index 1
120
125
 
@@ -142,24 +147,24 @@ while read service; do
142
147
  log_file="$repo_root/.harbor/${session_name}-${name}.log"
143
148
  : > "$log_file"
144
149
  # Use pipe-pane to capture ALL terminal output (works with any program, no buffering issues)
145
- tmux new-window -t "$session_name":$window_index -n "$name"
146
- tmux pipe-pane -t "$session_name":$window_index "cat >> \"$log_file\""
147
- tmux send-keys -t "$session_name":$window_index "cd \"$path\" && $command" C-m
150
+ $tmux_cmd new-window -t "$session_name":$window_index -n "$name"
151
+ $tmux_cmd pipe-pane -t "$session_name":$window_index "cat >> \"$log_file\""
152
+ $tmux_cmd send-keys -t "$session_name":$window_index "cd \"$path\" && $command" C-m
148
153
  # Start background process to trim logs if they get too large
149
154
  start_log_trim "$log_file" "$effective_max_lines"
150
155
  else
151
- tmux new-window -t "$session_name":$window_index -n "$name"
152
- tmux send-keys -t "$session_name":$window_index "cd \"$path\" && $command" C-m
156
+ $tmux_cmd new-window -t "$session_name":$window_index -n "$name"
157
+ $tmux_cmd send-keys -t "$session_name":$window_index "cd \"$path\" && $command" C-m
153
158
  fi
154
159
 
155
160
  ((window_index++))
156
161
  done < <(get_harbor_config | jq -c '.services[]')
157
162
 
158
163
  # Bind 'Home' key to switch to the terminal window
159
- tmux bind-key -n Home select-window -t :0
164
+ $tmux_cmd bind-key -n Home select-window -t :0
160
165
 
161
166
  # Select the terminal window
162
- tmux select-window -t "$session_name":0
167
+ $tmux_cmd select-window -t "$session_name":0
163
168
 
164
169
  # Attach to the tmux session (unless running in detached/headless mode)
165
170
  if [ "${HARBOR_DETACH:-0}" = "1" ]; then
@@ -179,5 +184,5 @@ if [ "${HARBOR_DETACH:-0}" = "1" ]; then
179
184
  echo ""
180
185
  fi
181
186
  else
182
- tmux attach-session -t "$session_name"
187
+ $tmux_cmd attach-session -t "$session_name"
183
188
  fi