@abyrd9/harbor-cli 2.3.1 → 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.
- package/dist/index.js +246 -239
- package/package.json +3 -2
- 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
|
-
|
|
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,219 +215,208 @@ 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
|
-
|
|
356
|
-
PREREQUISITES: Services must be running (started with 'harbor launch').
|
|
357
|
-
|
|
358
|
-
EXAMPLES:
|
|
359
|
-
harbor launch -d # Start in background
|
|
360
|
-
harbor anchor # Attach to default session
|
|
361
|
-
harbor anchor --name my-app # Attach to a specific named session`)
|
|
362
|
-
.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')
|
|
363
348
|
.action(async (options) => {
|
|
364
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
|
+
}
|
|
365
359
|
const config = await readHarborConfig();
|
|
366
360
|
const sessionName = options.name || config.sessionName || 'harbor';
|
|
361
|
+
const socketName = `harbor-${sessionName}`;
|
|
367
362
|
// Check if session exists
|
|
368
|
-
const checkSession = spawn('tmux', ['has-session', '-t', sessionName], {
|
|
363
|
+
const checkSession = spawn('tmux', ['-L', socketName, 'has-session', '-t', sessionName], {
|
|
369
364
|
stdio: 'pipe',
|
|
370
365
|
});
|
|
371
366
|
await new Promise((resolve) => {
|
|
372
367
|
checkSession.on('close', (code) => {
|
|
373
368
|
if (code !== 0) {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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');
|
|
377
373
|
process.exit(1);
|
|
378
374
|
}
|
|
379
375
|
resolve();
|
|
380
376
|
});
|
|
381
377
|
});
|
|
382
378
|
// Attach to the session
|
|
383
|
-
const attach = spawn('tmux', ['attach-session', '-t', sessionName], {
|
|
379
|
+
const attach = spawn('tmux', ['-L', socketName, 'attach-session', '-t', sessionName], {
|
|
384
380
|
stdio: 'inherit',
|
|
385
381
|
});
|
|
386
|
-
attach.on('close', (code) => {
|
|
382
|
+
attach.on('close', async (code) => {
|
|
383
|
+
// Check if session was killed (vs just detached)
|
|
384
|
+
const checkAfter = spawn('tmux', ['-L', socketName, 'has-session', '-t', sessionName], {
|
|
385
|
+
stdio: 'pipe',
|
|
386
|
+
});
|
|
387
|
+
const sessionStillExists = await new Promise((resolve) => {
|
|
388
|
+
checkAfter.on('close', (checkCode) => {
|
|
389
|
+
resolve(checkCode === 0);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
// If session no longer exists, it was killed - run after scripts
|
|
393
|
+
if (!sessionStillExists && config.after && config.after.length > 0) {
|
|
394
|
+
try {
|
|
395
|
+
await execute(config.after, 'after');
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
log.error('After scripts failed');
|
|
399
|
+
process.exit(1);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
387
402
|
process.exit(code || 0);
|
|
388
403
|
});
|
|
389
404
|
}
|
|
390
405
|
catch (err) {
|
|
391
|
-
|
|
406
|
+
log.error(err instanceof Error ? err.message : 'Unknown error');
|
|
392
407
|
process.exit(1);
|
|
393
408
|
}
|
|
394
409
|
});
|
|
395
410
|
program.command('scuttle')
|
|
396
|
-
.description(
|
|
397
|
-
|
|
398
|
-
WHEN TO USE: When you want to stop all services and free up resources.
|
|
399
|
-
|
|
400
|
-
WHAT IT DOES:
|
|
401
|
-
- Finds the running Harbor tmux session
|
|
402
|
-
- Kills the entire session (all service windows)
|
|
403
|
-
- All services stop immediately
|
|
404
|
-
|
|
405
|
-
SAFE TO RUN: If no session is running, it simply reports that and exits cleanly.
|
|
406
|
-
|
|
407
|
-
EXAMPLES:
|
|
408
|
-
harbor scuttle # Stop default session
|
|
409
|
-
harbor scuttle --name my-app # Stop a specific named session`)
|
|
410
|
-
.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')
|
|
411
413
|
.action(async (options) => {
|
|
412
414
|
try {
|
|
413
415
|
const config = await readHarborConfig();
|
|
414
416
|
const sessionName = options.name || config.sessionName || 'harbor';
|
|
417
|
+
const socketName = `harbor-${sessionName}`;
|
|
415
418
|
// Check if session exists
|
|
416
|
-
const checkSession = spawn('tmux', ['has-session', '-t', sessionName], {
|
|
419
|
+
const checkSession = spawn('tmux', ['-L', socketName, 'has-session', '-t', sessionName], {
|
|
417
420
|
stdio: 'pipe',
|
|
418
421
|
});
|
|
419
422
|
const sessionExists = await new Promise((resolve) => {
|
|
@@ -422,58 +425,48 @@ EXAMPLES:
|
|
|
422
425
|
});
|
|
423
426
|
});
|
|
424
427
|
if (!sessionExists) {
|
|
425
|
-
|
|
428
|
+
log.info(`No running Harbor session found ${pc.dim(`(looking for: ${sessionName})`)}`);
|
|
426
429
|
process.exit(0);
|
|
427
430
|
}
|
|
428
431
|
// Kill the session
|
|
429
|
-
const killSession = spawn('tmux', ['kill-session', '-t', sessionName], {
|
|
432
|
+
const killSession = spawn('tmux', ['-L', socketName, 'kill-session', '-t', sessionName], {
|
|
430
433
|
stdio: 'inherit',
|
|
431
434
|
});
|
|
432
|
-
killSession.on('close', (code) => {
|
|
435
|
+
killSession.on('close', async (code) => {
|
|
433
436
|
if (code === 0) {
|
|
434
|
-
|
|
437
|
+
log.success(`Harbor session ${pc.cyan(sessionName)} stopped`);
|
|
438
|
+
// Execute after scripts when session is killed
|
|
439
|
+
if (config.after && config.after.length > 0) {
|
|
440
|
+
try {
|
|
441
|
+
await execute(config.after, 'after');
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
log.error('After scripts failed');
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
435
448
|
}
|
|
436
449
|
else {
|
|
437
|
-
|
|
450
|
+
log.error('Failed to stop Harbor session');
|
|
438
451
|
}
|
|
439
452
|
process.exit(code || 0);
|
|
440
453
|
});
|
|
441
454
|
}
|
|
442
455
|
catch (err) {
|
|
443
|
-
|
|
456
|
+
log.error(err instanceof Error ? err.message : 'Unknown error');
|
|
444
457
|
process.exit(1);
|
|
445
458
|
}
|
|
446
459
|
});
|
|
447
460
|
program.command('bearings')
|
|
448
|
-
.description(
|
|
449
|
-
|
|
450
|
-
WHEN TO USE: To check if services are running, especially in headless mode.
|
|
451
|
-
|
|
452
|
-
WHAT IT SHOWS:
|
|
453
|
-
- Session name and running status
|
|
454
|
-
- List of service windows (with 📄 indicator if logging enabled)
|
|
455
|
-
- Log file paths and sizes
|
|
456
|
-
- Available commands (anchor, scuttle)
|
|
457
|
-
|
|
458
|
-
OUTPUT EXAMPLE:
|
|
459
|
-
⚓ Harbor Status
|
|
460
|
-
Session: harbor
|
|
461
|
-
Status: Running ✓
|
|
462
|
-
Services: [0] Terminal, [1] web 📄, [2] api 📄
|
|
463
|
-
Logs: .harbor/harbor-web.log (1.2 KB)
|
|
464
|
-
|
|
465
|
-
SAFE TO RUN: Works whether services are running or not.
|
|
466
|
-
|
|
467
|
-
EXAMPLES:
|
|
468
|
-
harbor bearings # Check default session
|
|
469
|
-
harbor bearings --name my-app # Check a specific named session`)
|
|
470
|
-
.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')
|
|
471
463
|
.action(async (options) => {
|
|
472
464
|
try {
|
|
473
465
|
const config = await readHarborConfig();
|
|
474
466
|
const sessionName = options.name || config.sessionName || 'harbor';
|
|
467
|
+
const socketName = `harbor-${sessionName}`;
|
|
475
468
|
// Check if session exists
|
|
476
|
-
const checkSession = spawn('tmux', ['has-session', '-t', sessionName], {
|
|
469
|
+
const checkSession = spawn('tmux', ['-L', socketName, 'has-session', '-t', sessionName], {
|
|
477
470
|
stdio: 'pipe',
|
|
478
471
|
});
|
|
479
472
|
const sessionExists = await new Promise((resolve) => {
|
|
@@ -482,16 +475,19 @@ EXAMPLES:
|
|
|
482
475
|
});
|
|
483
476
|
});
|
|
484
477
|
if (!sessionExists) {
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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('');
|
|
491
487
|
process.exit(0);
|
|
492
488
|
}
|
|
493
489
|
// Get list of windows (services)
|
|
494
|
-
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}'], {
|
|
495
491
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
496
492
|
});
|
|
497
493
|
let windowOutput = '';
|
|
@@ -502,38 +498,43 @@ EXAMPLES:
|
|
|
502
498
|
listWindows.on('close', () => resolve());
|
|
503
499
|
});
|
|
504
500
|
const windows = windowOutput.trim().split('\n').filter(Boolean);
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
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:')}`);
|
|
510
508
|
for (const window of windows) {
|
|
511
|
-
const [index, name
|
|
509
|
+
const [index, name] = window.split('|');
|
|
512
510
|
const logFile = `.harbor/${sessionName}-${name}.log`;
|
|
513
511
|
const hasLog = fs.existsSync(path.join(process.cwd(), logFile));
|
|
514
|
-
const logIndicator = hasLog ?
|
|
515
|
-
|
|
512
|
+
const logIndicator = hasLog ? pc.dim(' 📄') : '';
|
|
513
|
+
log.plain(` ${pc.dim(`[${index}]`)} ${pc.cyan(name)}${logIndicator}`);
|
|
516
514
|
}
|
|
517
515
|
// Check for log files
|
|
518
516
|
const harborDir = path.join(process.cwd(), '.harbor');
|
|
519
517
|
if (fs.existsSync(harborDir)) {
|
|
520
518
|
const logFiles = fs.readdirSync(harborDir).filter(f => f.endsWith('.log'));
|
|
521
519
|
if (logFiles.length > 0) {
|
|
522
|
-
|
|
520
|
+
log.plain('');
|
|
521
|
+
log.plain(` ${pc.dim('Logs:')}`);
|
|
523
522
|
for (const logFile of logFiles) {
|
|
524
523
|
const logPath = path.join(harborDir, logFile);
|
|
525
524
|
const stats = fs.statSync(logPath);
|
|
526
525
|
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
527
|
-
|
|
526
|
+
log.plain(` ${pc.dim(`.harbor/${logFile}`)} ${pc.dim(`(${sizeKB} KB)`)}`);
|
|
528
527
|
}
|
|
529
528
|
}
|
|
530
529
|
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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('');
|
|
534
535
|
}
|
|
535
536
|
catch (err) {
|
|
536
|
-
|
|
537
|
+
log.error(err instanceof Error ? err.message : 'Unknown error');
|
|
537
538
|
process.exit(1);
|
|
538
539
|
}
|
|
539
540
|
});
|
|
@@ -603,7 +604,7 @@ async function generateDevFile(dirPath) {
|
|
|
603
604
|
try {
|
|
604
605
|
const existing = await fs.promises.readFile('harbor.json', 'utf-8');
|
|
605
606
|
config = JSON.parse(existing);
|
|
606
|
-
|
|
607
|
+
log.info('Found existing harbor.json, scanning for new services...');
|
|
607
608
|
}
|
|
608
609
|
catch (err) {
|
|
609
610
|
if (err.code !== 'ENOENT') {
|
|
@@ -617,7 +618,7 @@ async function generateDevFile(dirPath) {
|
|
|
617
618
|
// Existing harbor config in package.json, use it
|
|
618
619
|
config = packageJson.harbor;
|
|
619
620
|
writeToPackageJson = true;
|
|
620
|
-
|
|
621
|
+
log.info('Found existing harbor config in package.json, scanning for new services...');
|
|
621
622
|
}
|
|
622
623
|
else {
|
|
623
624
|
// package.json exists but no harbor config - ask user where to store it
|
|
@@ -628,7 +629,7 @@ async function generateDevFile(dirPath) {
|
|
|
628
629
|
before: [],
|
|
629
630
|
after: [],
|
|
630
631
|
};
|
|
631
|
-
|
|
632
|
+
log.step(`Creating new harbor config in ${pc.cyan(choice)}...`);
|
|
632
633
|
}
|
|
633
634
|
}
|
|
634
635
|
catch (err) {
|
|
@@ -639,7 +640,7 @@ async function generateDevFile(dirPath) {
|
|
|
639
640
|
config = {
|
|
640
641
|
services: [],
|
|
641
642
|
};
|
|
642
|
-
|
|
643
|
+
log.step(`Creating new ${pc.cyan('harbor.json')}...`);
|
|
643
644
|
}
|
|
644
645
|
}
|
|
645
646
|
// Create a map of existing services for easy lookup
|
|
@@ -663,19 +664,19 @@ async function generateDevFile(dirPath) {
|
|
|
663
664
|
service.command = 'go run .';
|
|
664
665
|
}
|
|
665
666
|
config.services.push(service);
|
|
666
|
-
|
|
667
|
+
log.success(`Added service: ${pc.green(folder.name)}`);
|
|
667
668
|
newServicesAdded = true;
|
|
668
669
|
}
|
|
669
670
|
else if (existing.has(folder.name)) {
|
|
670
|
-
|
|
671
|
+
log.dim(` Skipping existing service: ${folder.name}`);
|
|
671
672
|
}
|
|
672
673
|
else {
|
|
673
|
-
|
|
674
|
+
log.dim(` Skipping directory ${folder.name} (no recognized project files)`);
|
|
674
675
|
}
|
|
675
676
|
}
|
|
676
677
|
}
|
|
677
678
|
if (!newServicesAdded) {
|
|
678
|
-
|
|
679
|
+
log.info('No new services found to add, feel free to add them manually');
|
|
679
680
|
}
|
|
680
681
|
const validationError = validateConfig(config);
|
|
681
682
|
if (validationError) {
|
|
@@ -687,15 +688,15 @@ async function generateDevFile(dirPath) {
|
|
|
687
688
|
const packageJson = JSON.parse(packageData);
|
|
688
689
|
packageJson.harbor = config;
|
|
689
690
|
await fs.promises.writeFile('package.json', JSON.stringify(packageJson, null, 2), 'utf-8');
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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(' }'));
|
|
699
700
|
}
|
|
700
701
|
else {
|
|
701
702
|
// Write to harbor.json with $schema for IntelliSense
|
|
@@ -704,10 +705,10 @@ async function generateDevFile(dirPath) {
|
|
|
704
705
|
...config,
|
|
705
706
|
};
|
|
706
707
|
await fs.promises.writeFile('harbor.json', JSON.stringify(configWithSchema, null, 2), 'utf-8');
|
|
707
|
-
|
|
708
|
+
log.success(`${pc.cyan('harbor.json')} created successfully`);
|
|
708
709
|
}
|
|
709
|
-
|
|
710
|
-
|
|
710
|
+
log.plain('');
|
|
711
|
+
log.info(`${pc.dim('Important:')} Verify the auto-detected commands are correct for your services`);
|
|
711
712
|
return true;
|
|
712
713
|
}
|
|
713
714
|
catch (err) {
|
|
@@ -754,11 +755,12 @@ async function execute(scripts, scriptType) {
|
|
|
754
755
|
if (!scripts || scripts.length === 0) {
|
|
755
756
|
return;
|
|
756
757
|
}
|
|
757
|
-
|
|
758
|
+
log.header(`Running ${scriptType} scripts...`);
|
|
758
759
|
for (let i = 0; i < scripts.length; i++) {
|
|
759
760
|
const script = scripts[i];
|
|
760
|
-
|
|
761
|
-
|
|
761
|
+
log.plain('');
|
|
762
|
+
log.step(`${pc.dim(`[${i + 1}/${scripts.length}]`)} ${pc.cyan(script.command)}`);
|
|
763
|
+
log.dim(` in ${script.path}`);
|
|
762
764
|
try {
|
|
763
765
|
await new Promise((resolve, reject) => {
|
|
764
766
|
const process = spawn('sh', ['-c', `cd "${script.path}" && ${script.command}`], {
|
|
@@ -766,7 +768,7 @@ async function execute(scripts, scriptType) {
|
|
|
766
768
|
});
|
|
767
769
|
process.on('close', (code) => {
|
|
768
770
|
if (code === 0) {
|
|
769
|
-
|
|
771
|
+
log.success(`${scriptType} script ${i + 1} completed`);
|
|
770
772
|
resolve(null);
|
|
771
773
|
}
|
|
772
774
|
else {
|
|
@@ -779,18 +781,20 @@ async function execute(scripts, scriptType) {
|
|
|
779
781
|
});
|
|
780
782
|
}
|
|
781
783
|
catch (err) {
|
|
782
|
-
|
|
784
|
+
log.error(`Error executing ${scriptType} script ${i + 1}: ${err instanceof Error ? err.message : 'Unknown error'}`);
|
|
783
785
|
throw err;
|
|
784
786
|
}
|
|
785
787
|
}
|
|
786
|
-
|
|
788
|
+
log.plain('');
|
|
789
|
+
log.success(`All ${scriptType} scripts completed`);
|
|
787
790
|
}
|
|
788
791
|
async function runServices(options = {}) {
|
|
789
792
|
const hasHarborConfig = checkHasHarborConfig();
|
|
790
793
|
if (!hasHarborConfig) {
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
+
log.error('No harbor configuration found');
|
|
795
|
+
log.plain('');
|
|
796
|
+
log.info('To initialize a new Harbor project:');
|
|
797
|
+
log.cmd('harbor dock');
|
|
794
798
|
process.exit(1);
|
|
795
799
|
}
|
|
796
800
|
// Load and validate config
|
|
@@ -799,12 +803,12 @@ async function runServices(options = {}) {
|
|
|
799
803
|
config = await readHarborConfig();
|
|
800
804
|
const validationError = validateConfig(config);
|
|
801
805
|
if (validationError) {
|
|
802
|
-
|
|
806
|
+
log.error(`Invalid harbor.json configuration: ${validationError}`);
|
|
803
807
|
process.exit(1);
|
|
804
808
|
}
|
|
805
809
|
}
|
|
806
810
|
catch (err) {
|
|
807
|
-
|
|
811
|
+
log.error(`Error reading config: ${err}`);
|
|
808
812
|
process.exit(1);
|
|
809
813
|
}
|
|
810
814
|
ensureLogSetup(config);
|
|
@@ -813,7 +817,7 @@ async function runServices(options = {}) {
|
|
|
813
817
|
await execute(config.before || [], 'before');
|
|
814
818
|
}
|
|
815
819
|
catch {
|
|
816
|
-
|
|
820
|
+
log.error('Before scripts failed, aborting launch');
|
|
817
821
|
process.exit(1);
|
|
818
822
|
}
|
|
819
823
|
// Ensure scripts exist and are executable
|
|
@@ -831,23 +835,26 @@ async function runServices(options = {}) {
|
|
|
831
835
|
});
|
|
832
836
|
return new Promise((resolve) => {
|
|
833
837
|
command.on('error', (err) => {
|
|
834
|
-
|
|
838
|
+
log.error(`Error running dev.sh: ${err}`);
|
|
835
839
|
process.exit(1);
|
|
836
840
|
});
|
|
837
841
|
command.on('close', async (code) => {
|
|
838
842
|
if (code !== 0) {
|
|
839
|
-
|
|
843
|
+
log.error(`dev.sh exited with code ${code}`);
|
|
840
844
|
process.exit(1);
|
|
841
845
|
}
|
|
842
|
-
//
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
846
|
+
// Only execute after scripts in attached mode
|
|
847
|
+
// In headless mode, after scripts are run by 'scuttle' when session is killed
|
|
848
|
+
if (!options.detach) {
|
|
849
|
+
try {
|
|
850
|
+
await execute(config.after || [], 'after');
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
log.error('After scripts failed');
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
850
856
|
}
|
|
857
|
+
resolve();
|
|
851
858
|
});
|
|
852
859
|
});
|
|
853
860
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abyrd9/harbor-cli",
|
|
3
|
-
"version": "2.3.
|
|
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
|
|
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,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
|
-
|
|
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 :+
|
|
95
100
|
|
|
96
101
|
# Configure status bar
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
152
|
-
|
|
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
|
-
|
|
164
|
+
$tmux_cmd bind-key -n Home select-window -t :0
|
|
160
165
|
|
|
161
166
|
# Select the terminal window
|
|
162
|
-
|
|
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
|
-
|
|
187
|
+
$tmux_cmd attach-session -t "$session_name"
|
|
183
188
|
fi
|