@abyrd9/harbor-cli 2.3.2 → 2.3.4
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 +208 -235
- package/package.json +3 -2
- package/scripts/dev.sh +56 -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
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
-
|
|
154
|
-
instructions.forEach(instruction => {
|
|
167
|
+
log.plain(pc.dim(' Installation options:'));
|
|
168
|
+
instructions.forEach(instruction => { log.plain(instruction); });
|
|
155
169
|
}
|
|
156
170
|
else {
|
|
157
|
-
|
|
171
|
+
log.plain(` ${pc.dim('Instructions:')} ${dep.installMsg}`);
|
|
158
172
|
}
|
|
159
173
|
}
|
|
160
|
-
|
|
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
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
245
|
+
${yellow('Options:')}
|
|
246
|
+
${cyan('-V, --version')} Show version
|
|
247
|
+
${cyan('-h, --help')} Show help for command
|
|
227
248
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
292
|
+
log.plain('');
|
|
293
|
+
log.success(pc.green('Environment prepared!'));
|
|
270
294
|
}
|
|
271
295
|
catch (err) {
|
|
272
|
-
|
|
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(
|
|
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
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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:
|
|
338
|
+
await runServices({ detach: isDetached, name: options.name });
|
|
339
339
|
}
|
|
340
340
|
catch (err) {
|
|
341
|
-
|
|
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(
|
|
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
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
444
|
+
log.error('After scripts failed');
|
|
463
445
|
process.exit(1);
|
|
464
446
|
}
|
|
465
447
|
}
|
|
466
448
|
}
|
|
467
449
|
else {
|
|
468
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
526
|
+
log.plain(` ${pc.dim(`.harbor/${logFile}`)} ${pc.dim(`(${sizeKB} KB)`)}`);
|
|
559
527
|
}
|
|
560
528
|
}
|
|
561
529
|
}
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
667
|
+
log.success(`Added service: ${pc.green(folder.name)}`);
|
|
698
668
|
newServicesAdded = true;
|
|
699
669
|
}
|
|
700
670
|
else if (existing.has(folder.name)) {
|
|
701
|
-
|
|
671
|
+
log.dim(` Skipping existing service: ${folder.name}`);
|
|
702
672
|
}
|
|
703
673
|
else {
|
|
704
|
-
|
|
674
|
+
log.dim(` Skipping directory ${folder.name} (no recognized project files)`);
|
|
705
675
|
}
|
|
706
676
|
}
|
|
707
677
|
}
|
|
708
678
|
if (!newServicesAdded) {
|
|
709
|
-
|
|
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
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
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
|
-
|
|
708
|
+
log.success(`${pc.cyan('harbor.json')} created successfully`);
|
|
739
709
|
}
|
|
740
|
-
|
|
741
|
-
|
|
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
|
-
|
|
758
|
+
log.header(`Running ${scriptType} scripts...`);
|
|
789
759
|
for (let i = 0; i < scripts.length; i++) {
|
|
790
760
|
const script = scripts[i];
|
|
791
|
-
|
|
792
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
823
|
-
|
|
824
|
-
|
|
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
|
-
|
|
806
|
+
log.error(`Invalid harbor.json configuration: ${validationError}`);
|
|
834
807
|
process.exit(1);
|
|
835
808
|
}
|
|
836
809
|
}
|
|
837
810
|
catch (err) {
|
|
838
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "2.3.4",
|
|
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
|
|
23
|
+
if $tmux_cmd has-session -t "$session_name" 2>/dev/null; then
|
|
19
24
|
echo "Killing existing tmux session '$session_name'"
|
|
20
|
-
|
|
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,76 @@ start_log_trim() {
|
|
|
51
56
|
trap cleanup_logs EXIT
|
|
52
57
|
|
|
53
58
|
# Start a new tmux session and rename the initial window
|
|
54
|
-
|
|
59
|
+
$tmux_cmd new-session -d -s "$session_name"
|
|
55
60
|
|
|
56
61
|
# Set tmux options
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
73
|
+
$tmux_cmd bind-key -n C-q kill-session
|
|
69
74
|
|
|
70
75
|
# Add padding and styling to panes
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
98
|
+
$tmux_cmd bind-key -n S-Left select-window -t :-
|
|
99
|
+
$tmux_cmd bind-key -n S-Right select-window -t :+
|
|
100
|
+
|
|
101
|
+
# Add Ctrl+t to create new terminal window (marked with + prefix)
|
|
102
|
+
$tmux_cmd bind-key -n C-t new-window -n "+Terminal"
|
|
103
|
+
|
|
104
|
+
# Add Ctrl+w to close current window ONLY if it's user-created (starts with +)
|
|
105
|
+
$tmux_cmd bind-key -n C-w if-shell 'tmux display-message -p "#{window_name}" | grep -q "^+"' 'kill-window' 'display-message "Cannot close service windows (only +Terminal tabs)"'
|
|
95
106
|
|
|
96
107
|
# Configure status bar
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
108
|
+
$tmux_cmd set-option -g status-position top
|
|
109
|
+
$tmux_cmd set-option -g status-style bg="#1c1917",fg="#a8a29e"
|
|
110
|
+
$tmux_cmd set-option -g status-left ""
|
|
111
|
+
$tmux_cmd set-option -g status-right "#[fg=#57534e]ctrl+t new · ctrl+w close · shift+←/→ switch · ctrl+q quit#[fg=#78716c] · %H:%M#[default]"
|
|
112
|
+
$tmux_cmd set-window-option -g window-status-current-format "\
|
|
102
113
|
#[fg=#6366f1, bg=#1c1917] →\
|
|
103
114
|
#[fg=#6366f1, bg=#1c1917, bold] #W\
|
|
104
115
|
#[fg=#6366f1, bg=#1c1917] "
|
|
105
|
-
|
|
116
|
+
$tmux_cmd set-window-option -g window-status-format "\
|
|
106
117
|
#[fg=#a8a29e, bg=#1c1917] \
|
|
107
118
|
#[fg=#a8a29e, bg=#1c1917] #W \
|
|
108
119
|
#[fg=#a8a29e, bg=#1c1917] "
|
|
109
120
|
|
|
110
121
|
# Add padding below status bar
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
122
|
+
$tmux_cmd set-option -g status 2
|
|
123
|
+
$tmux_cmd set-option -Fg 'status-format[1]' '#{status-format[0]}'
|
|
124
|
+
$tmux_cmd set-option -g 'status-format[0]' ''
|
|
114
125
|
|
|
115
126
|
# Create a new window for the interactive shell
|
|
116
127
|
echo "Creating window for interactive shell"
|
|
117
|
-
|
|
128
|
+
$tmux_cmd rename-window -t "$session_name":0 'Terminal'
|
|
118
129
|
|
|
119
130
|
window_index=1 # Start from index 1
|
|
120
131
|
|
|
@@ -142,24 +153,24 @@ while read service; do
|
|
|
142
153
|
log_file="$repo_root/.harbor/${session_name}-${name}.log"
|
|
143
154
|
: > "$log_file"
|
|
144
155
|
# Use pipe-pane to capture ALL terminal output (works with any program, no buffering issues)
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
$tmux_cmd new-window -t "$session_name":$window_index -n "$name"
|
|
157
|
+
$tmux_cmd pipe-pane -t "$session_name":$window_index "cat >> \"$log_file\""
|
|
158
|
+
$tmux_cmd send-keys -t "$session_name":$window_index "cd \"$path\" && $command" C-m
|
|
148
159
|
# Start background process to trim logs if they get too large
|
|
149
160
|
start_log_trim "$log_file" "$effective_max_lines"
|
|
150
161
|
else
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
$tmux_cmd new-window -t "$session_name":$window_index -n "$name"
|
|
163
|
+
$tmux_cmd send-keys -t "$session_name":$window_index "cd \"$path\" && $command" C-m
|
|
153
164
|
fi
|
|
154
165
|
|
|
155
166
|
((window_index++))
|
|
156
167
|
done < <(get_harbor_config | jq -c '.services[]')
|
|
157
168
|
|
|
158
169
|
# Bind 'Home' key to switch to the terminal window
|
|
159
|
-
|
|
170
|
+
$tmux_cmd bind-key -n Home select-window -t :0
|
|
160
171
|
|
|
161
172
|
# Select the terminal window
|
|
162
|
-
|
|
173
|
+
$tmux_cmd select-window -t "$session_name":0
|
|
163
174
|
|
|
164
175
|
# Attach to the tmux session (unless running in detached/headless mode)
|
|
165
176
|
if [ "${HARBOR_DETACH:-0}" = "1" ]; then
|
|
@@ -179,5 +190,5 @@ if [ "${HARBOR_DETACH:-0}" = "1" ]; then
|
|
|
179
190
|
echo ""
|
|
180
191
|
fi
|
|
181
192
|
else
|
|
182
|
-
|
|
193
|
+
$tmux_cmd attach-session -t "$session_name"
|
|
183
194
|
fi
|