@abyrd9/harbor-cli 0.0.1

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/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # Harbor CLI
2
+
3
+ A CLI tool for those small side projects that only run a few services. Harbor allows you to:
4
+
5
+ 1. ๐Ÿ› ๏ธ Define your services in a configuration file
6
+ 2. ๐Ÿ”„ Generate a Caddyfile to reverse proxy certain services to subdomains
7
+ 3. ๐Ÿš€ Launch your services in a tmux session with Caddy and your services automatically proxied
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ npm i -g harbor-cli
13
+ ```
14
+
15
+ ## Prerequisites
16
+
17
+ Before using Harbor, make sure you have the following installed:
18
+
19
+ - [Caddy](https://caddyserver.com/docs/install) (for reverse proxy)
20
+ - [tmux](https://github.com/tmux/tmux/wiki/Installing) (for terminal multiplexing)
21
+ - [jq](https://stedolan.github.io/jq/download/) (for JSON processing within tmux)
22
+
23
+ ## Quick Start
24
+
25
+ 1. Initialize your development environment:
26
+ ```bash
27
+ harbor dock
28
+ ```
29
+
30
+ 2. Add new services to your configuration:
31
+ ```bash
32
+ harbor moor
33
+ ```
34
+
35
+ 3. Update your Caddyfile:
36
+ ```bash
37
+ harbor anchor
38
+ ```
39
+
40
+ 4. Launch your services:
41
+ ```bash
42
+ harbor launch
43
+ ```
44
+
45
+ ## Configuration
46
+
47
+ Harbor uses two main configuration files:
48
+
49
+ ### harbor.json
50
+
51
+ Contains your service configurations that are used to generate the Caddyfile and launch the services:
52
+
53
+ ```json
54
+ {
55
+ "domain": "localhost",
56
+ "services": [
57
+ {
58
+ "name": "frontend",
59
+ "path": "./vite-frontend",
60
+ "command": "npm run dev",
61
+ "port": 3000,
62
+ "subdomain": "app"
63
+ },
64
+ {
65
+ "name": "api",
66
+ "path": "./go-api",
67
+ "command": "go run .",
68
+ "port": 8080,
69
+ "subdomain": "api"
70
+ },
71
+ {
72
+ "name": "dashboard",
73
+ "path": "./vite-frontend",
74
+ "command": "npx drizzle-kit studio",
75
+ }
76
+ ]
77
+ }
78
+ ```
79
+
80
+ > Note: The dashboard service is a bit special. This is a drizzle studio instance to view your database. There's no subdomain value and no port declared because it typically runs at `local.drizzle.studio`. This will still be running and viewable in your tmux session, but it won't be automatically proxied.
81
+
82
+ ### Caddyfile
83
+
84
+ Automatically generated reverse proxy configuration:
85
+
86
+ ```caddy
87
+ api.localhost {
88
+ reverse_proxy localhost:8080
89
+ }
90
+
91
+ app.localhost {
92
+ reverse_proxy localhost:3000
93
+ }
94
+ ```
95
+
96
+ ## Commands
97
+
98
+ - `harbor dock`: Generate a new harbor.json file
99
+ - `harbor moor`: Add new services to your harbor.json file
100
+ - `harbor anchor`: Update your Caddyfile from the current harbor.json file
101
+ - `harbor launch`: Start all services defined in your harbor.json file in a tmux session
102
+
103
+ ## Terminal Multiplexer
104
+
105
+ Harbor uses tmux for managing your services. Some useful shortcuts:
106
+
107
+ - `Ctrl+a d`: Detach from session
108
+ - `Ctrl+a c`: Create new window
109
+ - `Ctrl+a n`: Next window
110
+ - `Ctrl+a p`: Previous window
111
+ - `Ctrl+q`: Quit session
112
+
113
+ ## Contributing
114
+
115
+ Contributions are welcome! Please feel free to submit a Pull Request.
116
+
117
+ ## License
118
+
119
+ MIT
package/dist/index.js ADDED
@@ -0,0 +1,403 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from '@commander-js/extra-typings';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import { spawn } from 'node:child_process';
6
+ import { chmodSync } from 'node:fs';
7
+ import { readFileSync } from 'node:fs';
8
+ import { fileURLToPath } from 'node:url';
9
+ // Read version from package.json
10
+ const __filename = fileURLToPath(import.meta.url);
11
+ const __dirname = path.dirname(__filename);
12
+ const packageJson = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
13
+ const requiredDependencies = [
14
+ {
15
+ name: 'Caddy',
16
+ command: 'caddy version',
17
+ installMsg: 'https://caddyserver.com/docs/install',
18
+ requiredFor: 'reverse proxy functionality',
19
+ },
20
+ {
21
+ name: 'tmux',
22
+ command: 'tmux -V',
23
+ installMsg: 'https://github.com/tmux/tmux/wiki/Installing',
24
+ requiredFor: 'terminal multiplexing',
25
+ },
26
+ {
27
+ name: 'jq',
28
+ command: 'jq --version',
29
+ installMsg: 'https://stedolan.github.io/jq/download/',
30
+ requiredFor: 'JSON processing in service management',
31
+ },
32
+ ];
33
+ async function checkDependencies() {
34
+ const missingDeps = [];
35
+ for (const dep of requiredDependencies) {
36
+ try {
37
+ await new Promise((resolve, reject) => {
38
+ const process = spawn('sh', ['-c', dep.command]);
39
+ process.on('close', (code) => {
40
+ if (code === 0)
41
+ resolve(null);
42
+ else
43
+ reject();
44
+ });
45
+ });
46
+ }
47
+ catch {
48
+ missingDeps.push(dep);
49
+ }
50
+ }
51
+ if (missingDeps.length > 0) {
52
+ console.log('โŒ Missing required dependencies:');
53
+ for (const dep of missingDeps) {
54
+ console.log(`\n${dep.name} (required for ${dep.requiredFor})`);
55
+ console.log(`Install instructions: ${dep.installMsg}`);
56
+ }
57
+ throw new Error('Please install missing dependencies before continuing');
58
+ }
59
+ }
60
+ const possibleProjectFiles = [
61
+ 'package.json', // Node.js projects
62
+ 'go.mod', // Go projects
63
+ 'Cargo.toml', // Rust projects
64
+ 'composer.json', // PHP projects
65
+ 'requirements.txt', // Python projects
66
+ 'Gemfile', // Ruby projects
67
+ 'pom.xml', // Java Maven projects
68
+ 'build.gradle', // Java Gradle projects
69
+ ];
70
+ const program = new Command();
71
+ program
72
+ .name('harbor')
73
+ .description(`A CLI tool for managing your project's local development services
74
+
75
+ Harbor helps you manage multiple local development services with ease.
76
+ It provides a simple way to configure and run your services with automatic
77
+ subdomain routing through Caddy reverse proxy.
78
+
79
+ Available Commands:
80
+ dock Initialize a new Harbor project
81
+ moor Add new services to your configuration
82
+ anchor Generate Caddy reverse proxy configuration
83
+ launch Start all services in tmux sessions`)
84
+ .version(packageJson.version)
85
+ .action(async () => await checkDependencies())
86
+ .addHelpCommand(false);
87
+ // If no command is provided, display help
88
+ if (process.argv.length <= 2) {
89
+ program.help();
90
+ }
91
+ program.command('dock')
92
+ .description(`Prepares your development environment by creating both:
93
+ - harbor.json configuration file
94
+ - Caddyfile for reverse proxy
95
+
96
+ This is typically the first command you'll run in a new project.`)
97
+ .option('-p, --path <path>', 'The path to the root of your project', './')
98
+ .action(async (options) => {
99
+ const caddyFileExists = fileExists('Caddyfile');
100
+ const configFileExists = fileExists('harbor.json');
101
+ if (caddyFileExists || configFileExists) {
102
+ console.log('โŒ Error: Harbor project already initialized');
103
+ if (caddyFileExists) {
104
+ console.log(' - Caddyfile already exists');
105
+ }
106
+ if (configFileExists) {
107
+ console.log(' - harbor.json already exists');
108
+ }
109
+ console.log('\nTo reinitialize, please remove these files first.');
110
+ process.exit(1);
111
+ }
112
+ await generateDevFile(options.path);
113
+ await generateCaddyFile();
114
+ console.log('โœจ Environment successfully prepared and anchored!');
115
+ });
116
+ program.command('moor')
117
+ .description('Add new services to your harbor.json configuration file')
118
+ .option('-p, --path <path>', 'The path to the root of your project', './')
119
+ .action(async (options) => {
120
+ if (!fileExists('harbor.json')) {
121
+ console.log('โŒ No harbor.json configuration found');
122
+ console.log('\nTo initialize a new Harbor project, please use:');
123
+ console.log(' harbor anchor');
124
+ process.exit(1);
125
+ }
126
+ await generateDevFile(options.path);
127
+ });
128
+ program.command('anchor')
129
+ .description(`Add new services to your Caddyfile
130
+
131
+ Note: This command will stop any active Caddy processes, including those from other Harbor projects.`)
132
+ .action(async () => {
133
+ if (!fileExists('harbor.json')) {
134
+ console.log('โŒ No harbor.json configuration found');
135
+ console.log('\nTo initialize a new Harbor project, please use:');
136
+ console.log(' harbor anchor');
137
+ process.exit(1);
138
+ }
139
+ await generateCaddyFile();
140
+ });
141
+ program.command('launch')
142
+ .description(`Launch your services in the harbor terminal multiplexer (Using tmux)
143
+
144
+ Note: This command will stop any active Caddy processes, including those from other Harbor projects.`)
145
+ .action(async () => {
146
+ await runServices();
147
+ });
148
+ program.parse();
149
+ function fileExists(path) {
150
+ return fs.existsSync(`${process.cwd()}/${path}`);
151
+ }
152
+ function isProjectDirectory(dirPath) {
153
+ return possibleProjectFiles.some(file => {
154
+ try {
155
+ return fs.existsSync(path.join(process.cwd(), dirPath, file));
156
+ }
157
+ catch {
158
+ return false;
159
+ }
160
+ });
161
+ }
162
+ function validateConfig(config) {
163
+ if (!config.domain) {
164
+ return 'Domain is required';
165
+ }
166
+ if (!Array.isArray(config.services)) {
167
+ return 'Services must be an array';
168
+ }
169
+ for (const service of config.services) {
170
+ if (!service.name) {
171
+ return 'Service name is required';
172
+ }
173
+ if (!service.path) {
174
+ return 'Service path is required';
175
+ }
176
+ }
177
+ return null;
178
+ }
179
+ async function generateDevFile(dirPath) {
180
+ let config;
181
+ try {
182
+ const existing = await fs.promises.readFile('harbor.json', 'utf-8');
183
+ config = JSON.parse(existing);
184
+ console.log('Found existing harbor.json, scanning for new services...');
185
+ }
186
+ catch (err) {
187
+ if (err.code !== 'ENOENT') {
188
+ console.error('Error reading harbor.json:', err);
189
+ process.exit(1);
190
+ }
191
+ // Initialize new config with defaults
192
+ config = {
193
+ domain: 'localhost',
194
+ useSudo: false,
195
+ services: [],
196
+ };
197
+ console.log('Creating new harbor.json...');
198
+ }
199
+ // Create a map of existing services for easy lookup
200
+ const existing = new Set(config.services.map(s => s.name));
201
+ let newServicesAdded = false;
202
+ try {
203
+ const folders = await fs.promises.readdir(dirPath, { withFileTypes: true });
204
+ for (const folder of folders) {
205
+ if (folder.isDirectory()) {
206
+ const folderPath = path.join(dirPath, folder.name);
207
+ // Only add directories that contain project files and aren't already in config
208
+ if (isProjectDirectory(folderPath) && !existing.has(folder.name)) {
209
+ const service = {
210
+ name: folder.name,
211
+ path: folderPath,
212
+ subdomain: folder.name.toLowerCase().replace(/\s+/g, ''),
213
+ };
214
+ // Try to determine default command based on project type
215
+ if (fs.existsSync(path.join(folderPath, 'package.json'))) {
216
+ service.command = 'npm run dev';
217
+ }
218
+ else if (fs.existsSync(path.join(folderPath, 'go.mod'))) {
219
+ service.command = 'go run .';
220
+ }
221
+ config.services.push(service);
222
+ console.log(`Added new service: ${folder.name}`);
223
+ newServicesAdded = true;
224
+ }
225
+ else if (existing.has(folder.name)) {
226
+ console.log(`Skipping existing service: ${folder.name}`);
227
+ }
228
+ else {
229
+ console.log(`Skipping directory ${folder.name} (no recognized project files)`);
230
+ }
231
+ }
232
+ }
233
+ if (!newServicesAdded) {
234
+ console.log('No new services found to add, feel free to add them manually');
235
+ return;
236
+ }
237
+ const validationError = validateConfig(config);
238
+ if (validationError) {
239
+ console.log(`โŒ Invalid harbor.json configuration: ${validationError}`);
240
+ process.exit(1);
241
+ }
242
+ await fs.promises.writeFile('harbor.json', JSON.stringify(config, null, 2), 'utf-8');
243
+ console.log('\nharbor.json updated successfully');
244
+ console.log('\nImportant:');
245
+ console.log(' - Update the \'Port\' field for each service to match its actual port or leave blank to ignore in the Caddyfile');
246
+ console.log(' - Verify the auto-detected commands are correct for your services');
247
+ }
248
+ catch (err) {
249
+ console.error('Error processing directory:', err);
250
+ process.exit(1);
251
+ }
252
+ }
253
+ async function readHarborConfig() {
254
+ try {
255
+ const data = await fs.promises.readFile('harbor.json', 'utf-8');
256
+ const config = JSON.parse(data);
257
+ const validationError = validateConfig(config);
258
+ if (validationError) {
259
+ throw new Error(`Invalid configuration: ${validationError}`);
260
+ }
261
+ return config;
262
+ }
263
+ catch (err) {
264
+ if (err.code === 'ENOENT') {
265
+ throw new Error('harbor.json not found');
266
+ }
267
+ throw err;
268
+ }
269
+ }
270
+ async function stopCaddy() {
271
+ try {
272
+ console.log('\nโš ๏ธ Stopping any existing Caddy processes...');
273
+ console.log(' This will interrupt any active Harbor or Caddy services\n');
274
+ // Try to kill any existing Caddy processes
275
+ await new Promise((resolve) => {
276
+ const isWindows = process.platform === 'win32';
277
+ const killCommand = isWindows ? 'taskkill /F /IM caddy.exe' : 'pkill caddy';
278
+ const childProcess = spawn('sh', ['-c', killCommand]);
279
+ childProcess.on('close', () => {
280
+ // It's okay if there was no process to kill (code 1)
281
+ resolve();
282
+ });
283
+ });
284
+ // Give it a moment to fully release ports
285
+ await new Promise(resolve => setTimeout(resolve, 1000));
286
+ }
287
+ catch (err) {
288
+ // Ignore errors as the process might not exist
289
+ }
290
+ }
291
+ async function generateCaddyFile() {
292
+ try {
293
+ const config = await readHarborConfig();
294
+ let caddyfileContent = '';
295
+ for (const svc of config.services) {
296
+ if (!svc.port || !svc.subdomain) {
297
+ continue;
298
+ }
299
+ const serverName = `${svc.subdomain}.${config.domain}`;
300
+ caddyfileContent += `${serverName} {\n`;
301
+ caddyfileContent += ` reverse_proxy localhost:${svc.port}\n`;
302
+ caddyfileContent += "}\n\n";
303
+ }
304
+ await fs.promises.writeFile('Caddyfile', caddyfileContent, 'utf-8');
305
+ // Stop existing Caddy process before proceeding
306
+ await stopCaddy();
307
+ console.log('Caddyfile generated successfully');
308
+ }
309
+ catch (err) {
310
+ console.error('Error generating Caddyfile:', err);
311
+ process.exit(1);
312
+ }
313
+ }
314
+ async function runServices() {
315
+ // Check for required files
316
+ if (!fileExists('harbor.json')) {
317
+ console.log('โŒ No harbor.json configuration found');
318
+ console.log('\nTo initialize a new Harbor project, please use:');
319
+ console.log(' harbor anchor');
320
+ process.exit(1);
321
+ }
322
+ // Load and validate config
323
+ try {
324
+ const config = await readHarborConfig();
325
+ const validationError = validateConfig(config);
326
+ if (validationError) {
327
+ console.log(`โŒ Invalid harbor.json configuration: ${validationError}`);
328
+ process.exit(1);
329
+ }
330
+ }
331
+ catch (err) {
332
+ console.error('Error reading config:', err);
333
+ process.exit(1);
334
+ }
335
+ if (!fileExists('Caddyfile')) {
336
+ console.log('โŒ No Caddyfile found');
337
+ console.log('\nTo initialize a new Harbor project, please use:');
338
+ console.log(' harbor anchor');
339
+ console.log('\nOr to generate just the Caddyfile:');
340
+ console.log(' harbor moor');
341
+ process.exit(1);
342
+ }
343
+ // Stop any existing Caddy process
344
+ await stopCaddy();
345
+ // Ensure scripts exist and are executable
346
+ await ensureScriptsExist();
347
+ // Execute the script directly using spawn to handle I/O streams
348
+ const scriptPath = path.join(getScriptsDir(), 'dev.sh');
349
+ const command = spawn('bash', [scriptPath], {
350
+ stdio: 'inherit', // This will pipe stdin/stdout/stderr to the parent process
351
+ });
352
+ return new Promise((resolve, reject) => {
353
+ command.on('error', (err) => {
354
+ console.error(`Error running dev.sh: ${err}`);
355
+ process.exit(1);
356
+ });
357
+ command.on('close', (code) => {
358
+ if (code !== 0) {
359
+ console.error(`dev.sh exited with code ${code}`);
360
+ process.exit(1);
361
+ }
362
+ resolve();
363
+ });
364
+ });
365
+ }
366
+ // Get the package root directory
367
+ function getPackageRoot() {
368
+ const __filename = fileURLToPath(import.meta.url);
369
+ const __dirname = path.dirname(__filename);
370
+ return path.join(__dirname, '..');
371
+ }
372
+ // Get the template scripts directory (where our source scripts live)
373
+ function getTemplateScriptsDir() {
374
+ return path.join(getPackageRoot(), 'scripts');
375
+ }
376
+ // Get the scripts directory (where we'll create the scripts for the user)
377
+ function getScriptsDir() {
378
+ return path.join(getPackageRoot(), 'scripts');
379
+ }
380
+ async function ensureScriptsExist() {
381
+ const scriptsDir = getScriptsDir();
382
+ const templateDir = getTemplateScriptsDir();
383
+ // Ensure scripts directory exists
384
+ if (!fs.existsSync(scriptsDir)) {
385
+ fs.mkdirSync(scriptsDir, { recursive: true });
386
+ }
387
+ try {
388
+ const scriptPath = path.join(scriptsDir, 'dev.sh');
389
+ const templatePath = path.join(templateDir, 'dev.sh');
390
+ // Create the script if it doesn't exist
391
+ if (!fs.existsSync(scriptPath)) {
392
+ const templateContent = readFileSync(templatePath, 'utf-8');
393
+ fs.writeFileSync(scriptPath, templateContent, 'utf-8');
394
+ console.log('Created dev.sh');
395
+ }
396
+ // Make the script executable
397
+ chmodSync(scriptPath, '755');
398
+ }
399
+ catch (err) {
400
+ console.error('Error setting up dev.sh:', err);
401
+ throw err;
402
+ }
403
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@abyrd9/harbor-cli",
3
+ "version": "0.0.1",
4
+ "description": "A CLI tool for managing local development services with automatic subdomain routing",
5
+ "type": "module",
6
+ "bin": {
7
+ "harbor": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist",
11
+ "scripts"
12
+ ],
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "prepare": "bun run build",
16
+ "start": "node dist/index.js",
17
+ "harbor": "node dist/index.js",
18
+ "prepublishOnly": "npm run build",
19
+ "release": "npm run build && changeset publish",
20
+ "changeset": "changeset",
21
+ "version": "changeset version"
22
+ },
23
+ "keywords": [
24
+ "cli",
25
+ "development",
26
+ "proxy",
27
+ "caddy",
28
+ "tmux",
29
+ "local-development"
30
+ ],
31
+ "author": "Andrew Byrd",
32
+ "license": "MIT",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "git+https://github.com/Abyrd9/harbor-cli.git"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/Abyrd9/harbor-cli/issues"
39
+ },
40
+ "homepage": "https://github.com/Abyrd9/harbor-cli#readme",
41
+ "engines": {
42
+ "node": ">=18.0.0"
43
+ },
44
+ "dependencies": {
45
+ "@commander-js/extra-typings": "^11.1.0"
46
+ },
47
+ "devDependencies": {
48
+ "@changesets/cli": "^2.27.10",
49
+ "@types/node": "^20.10.5",
50
+ "bun-types": "latest",
51
+ "typescript": "^5.3.3"
52
+ },
53
+ "main": "index.js"
54
+ }
package/scripts/dev.sh ADDED
@@ -0,0 +1,100 @@
1
+ #!/bin/bash
2
+
3
+ # Check if the session already exists and kill it
4
+ if tmux has-session -t local-dev-test 2>/dev/null; then
5
+ echo "Killing existing tmux session 'local-dev-test'"
6
+ tmux kill-session -t local-dev-test
7
+ fi
8
+
9
+ # Start a new tmux session named 'local-dev-test' and rename the initial window
10
+ tmux new-session -d -s local-dev-test
11
+
12
+ # Set tmux options
13
+ tmux set-option -g prefix C-a
14
+ tmux bind-key C-a send-prefix
15
+ tmux set-option -g mouse on
16
+ tmux set-option -g history-limit 50000
17
+ tmux set-window-option -g mode-keys vi
18
+
19
+ # Add binding to kill session with Ctrl+q
20
+ tmux bind-key -n C-q kill-session
21
+
22
+ # Add padding and styling to panes
23
+ tmux set-option -g pane-border-style fg="#3f3f3f"
24
+ tmux set-option -g pane-active-border-style fg="#6366f1"
25
+ tmux set-option -g pane-border-status top
26
+ tmux set-option -g pane-border-format ""
27
+
28
+ # Add padding inside panes
29
+ tmux set-option -g status-left-length 100
30
+ tmux set-option -g status-right-length 100
31
+ tmux set-window-option -g window-style 'fg=colour247,bg=colour236'
32
+ tmux set-window-option -g window-active-style 'fg=colour250,bg=black'
33
+
34
+ # Set inner padding
35
+ tmux set-option -g window-style "bg=#1c1917 fg=#a8a29e"
36
+ tmux set-option -g window-active-style "bg=#1c1917 fg=#ffffff"
37
+
38
+ # Improve copy mode and mouse behavior
39
+ tmux set-option -g set-clipboard external
40
+ tmux bind-key -T copy-mode-vi v send-keys -X begin-selection
41
+ tmux bind-key -T copy-mode-vi y send-keys -X copy-selection-and-cancel
42
+ tmux bind-key -T copy-mode-vi MouseDragEnd1Pane send-keys -X copy-pipe-and-cancel "pbcopy"
43
+
44
+ # Set easier window navigation shortcuts
45
+ tmux bind-key -n Left select-window -t :-
46
+ tmux bind-key -n Right select-window -t :+
47
+
48
+ # Configure status bar
49
+ tmux set-option -g status-position top
50
+ tmux set-option -g status-style bg="#1c1917",fg="#a8a29e"
51
+ tmux set-option -g status-left ""
52
+ tmux set-option -g status-right "#[fg=#a8a29e]Close with ctrl+q ยท #[fg=white]%H:%M#[default]"
53
+ tmux set-window-option -g window-status-current-format "\
54
+ #[fg=#6366f1, bg=#1c1917] โ†’
55
+ #[fg=#6366f1, bg=#1c1917, bold] #W
56
+ #[fg=#6366f1, bg=#1c1917] "
57
+ tmux set-window-option -g window-status-format "\
58
+ #[fg=#a8a29e, bg=#1c1917]
59
+ #[fg=#a8a29e, bg=#1c1917] #W \
60
+ #[fg=#a8a29e, bg=#1c1917] "
61
+
62
+ # Add padding below status bar
63
+ tmux set-option -g status 2
64
+ tmux set-option -Fg 'status-format[1]' '#{status-format[0]}'
65
+ tmux set-option -g 'status-format[0]' ''
66
+
67
+ # Create a new window for the interactive shell
68
+ echo "Creating window for interactive shell"
69
+ tmux rename-window -t local-dev-test:0 'Terminal'
70
+
71
+ # Create a new window for Caddy (now on index 1)
72
+ echo "Creating window for Caddy"
73
+ tmux new-window -t local-dev-test:1 -n 'Caddy'
74
+ tmux send-keys -t local-dev-test:1 'caddy run' C-m
75
+
76
+ # Create windows dynamically based on harbor.json
77
+ window_index=2 # Start from index 2 since we have Terminal and Caddy
78
+ jq -c '.services[]' harbor.json | while read service; do
79
+ name=$(echo $service | jq -r '.name')
80
+ path=$(echo $service | jq -r '.path')
81
+ command=$(echo $service | jq -r '.command')
82
+
83
+ echo "Creating window for service: $name"
84
+ echo "Path: $path"
85
+ echo "Command: $command"
86
+
87
+ tmux new-window -t local-dev-test:$window_index -n "$name"
88
+ tmux send-keys -t local-dev-test:$window_index "cd $path && $command" C-m
89
+
90
+ ((window_index++))
91
+ done
92
+
93
+ # Bind 'Home' key to switch to the terminal window
94
+ tmux bind-key -n Home select-window -t :0
95
+
96
+ # Select the terminal window
97
+ tmux select-window -t local-dev-test:0
98
+
99
+ # Attach to the tmux session
100
+ tmux attach-session -t local-dev-test