@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 +119 -0
- package/dist/index.js +403 -0
- package/package.json +54 -0
- package/scripts/dev.sh +100 -0
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
|