@abyrd9/harbor-cli 0.1.1 → 1.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 +7 -38
- package/dist/index.js +7 -150
- package/package.json +7 -15
- package/scripts/dev.sh +0 -28
package/README.md
CHANGED
|
@@ -3,8 +3,7 @@
|
|
|
3
3
|
A CLI tool for those small side projects that only run a few services. Harbor allows you to:
|
|
4
4
|
|
|
5
5
|
1. 🛠️ Define your services in a configuration file
|
|
6
|
-
2.
|
|
7
|
-
3. 🚀 Launch your services in a tmux session with Caddy and your services automatically proxied
|
|
6
|
+
2. 🚀 Launch your services in a tmux session
|
|
8
7
|
|
|
9
8
|
## Installation
|
|
10
9
|
|
|
@@ -16,7 +15,6 @@ npm i -g @abyrd9/harbor-cli
|
|
|
16
15
|
|
|
17
16
|
Before using Harbor, make sure you have the following installed:
|
|
18
17
|
|
|
19
|
-
- [Caddy](https://caddyserver.com/docs/install) (for reverse proxy)
|
|
20
18
|
- [tmux](https://github.com/tmux/tmux/wiki/Installing) (for terminal multiplexing)
|
|
21
19
|
- [jq](https://stedolan.github.io/jq/download/) (for JSON processing within tmux)
|
|
22
20
|
|
|
@@ -32,74 +30,45 @@ harbor dock
|
|
|
32
30
|
harbor moor
|
|
33
31
|
```
|
|
34
32
|
|
|
35
|
-
3.
|
|
36
|
-
```bash
|
|
37
|
-
harbor anchor
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
4. Launch your services:
|
|
33
|
+
3. Launch your services:
|
|
41
34
|
```bash
|
|
42
35
|
harbor launch
|
|
43
36
|
```
|
|
44
37
|
|
|
45
38
|
## Configuration
|
|
46
39
|
|
|
47
|
-
Harbor uses
|
|
40
|
+
Harbor uses a configuration file to manage your services:
|
|
48
41
|
|
|
49
42
|
### harbor.json
|
|
50
43
|
|
|
51
|
-
Contains your service configurations that are used to
|
|
44
|
+
Contains your service configurations that are used to launch the services:
|
|
52
45
|
|
|
53
46
|
```json
|
|
54
47
|
{
|
|
55
|
-
"domain": "localhost",
|
|
56
48
|
"services": [
|
|
57
49
|
{
|
|
58
50
|
"name": "frontend",
|
|
59
51
|
"path": "./vite-frontend",
|
|
60
|
-
"command": "npm run dev"
|
|
61
|
-
"port": 3000,
|
|
62
|
-
"subdomain": "app"
|
|
52
|
+
"command": "npm run dev"
|
|
63
53
|
},
|
|
64
54
|
{
|
|
65
55
|
"name": "api",
|
|
66
56
|
"path": "./go-api",
|
|
67
|
-
"command": "go run ."
|
|
68
|
-
"port": 8080,
|
|
69
|
-
"subdomain": "api"
|
|
57
|
+
"command": "go run ."
|
|
70
58
|
},
|
|
71
59
|
{
|
|
72
60
|
"name": "dashboard",
|
|
73
61
|
"path": "./vite-frontend",
|
|
74
|
-
"command": "npx drizzle-kit studio"
|
|
62
|
+
"command": "npx drizzle-kit studio"
|
|
75
63
|
}
|
|
76
64
|
]
|
|
77
65
|
}
|
|
78
66
|
```
|
|
79
67
|
|
|
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
|
-
> Note: Some services may be tricky with Caddy and getting HMR to work or whatever other problems. Feel free to tinker with the Caddyfile or the subdomain values in the harbor config file in order to get things running. Maybe in the future there will be examples for common cases.
|
|
97
|
-
|
|
98
68
|
## Commands
|
|
99
69
|
|
|
100
70
|
- `harbor dock`: Generate a new harbor.json file
|
|
101
71
|
- `harbor moor`: Add new services to your harbor.json file
|
|
102
|
-
- `harbor anchor`: Update your Caddyfile from the current harbor.json file
|
|
103
72
|
- `harbor launch`: Start all services defined in your harbor.json file in a tmux session
|
|
104
73
|
|
|
105
74
|
## Terminal Multiplexer
|
package/dist/index.js
CHANGED
|
@@ -11,12 +11,6 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
11
11
|
const __dirname = path.dirname(__filename);
|
|
12
12
|
const packageJson = JSON.parse(readFileSync(path.join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
13
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
14
|
{
|
|
21
15
|
name: 'tmux',
|
|
22
16
|
command: 'tmux -V',
|
|
@@ -73,13 +67,11 @@ program
|
|
|
73
67
|
.description(`A CLI tool for managing your project's local development services
|
|
74
68
|
|
|
75
69
|
Harbor helps you manage multiple local development services with ease.
|
|
76
|
-
It provides a simple way to configure and run your services
|
|
77
|
-
subdomain routing through Caddy reverse proxy.
|
|
70
|
+
It provides a simple way to configure and run your services in tmux sessions.
|
|
78
71
|
|
|
79
72
|
Available Commands:
|
|
80
73
|
dock Initialize a new Harbor project
|
|
81
74
|
moor Add new services to your configuration
|
|
82
|
-
anchor Generate Caddy reverse proxy configuration
|
|
83
75
|
launch Start all services in tmux sessions`)
|
|
84
76
|
.version(packageJson.version)
|
|
85
77
|
.action(async () => await checkDependencies())
|
|
@@ -89,48 +81,22 @@ if (process.argv.length <= 2) {
|
|
|
89
81
|
program.help();
|
|
90
82
|
}
|
|
91
83
|
program.command('dock')
|
|
92
|
-
.description(`Prepares your development environment by creating
|
|
84
|
+
.description(`Prepares your development environment by creating a harbor configuration file
|
|
93
85
|
- harbor.json configuration file (or harbor field in package.json)
|
|
94
|
-
- Caddyfile for reverse proxy (if needed)
|
|
95
86
|
|
|
96
87
|
This is typically the first command you'll run in a new project.`)
|
|
97
88
|
.option('-p, --path <path>', 'The path to the root of your project', './')
|
|
98
89
|
.action(async (options) => {
|
|
99
90
|
try {
|
|
100
|
-
const caddyFileExists = fileExists('Caddyfile');
|
|
101
91
|
const configExists = checkHasHarborConfig();
|
|
102
|
-
if (
|
|
92
|
+
if (configExists) {
|
|
103
93
|
console.log('❌ Error: Harbor project already initialized');
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
}
|
|
107
|
-
if (configExists) {
|
|
108
|
-
console.log(' - Harbor configuration already exists');
|
|
109
|
-
}
|
|
110
|
-
console.log('\nTo reinitialize, please remove these files first.');
|
|
94
|
+
console.log(' - Harbor configuration already exists');
|
|
95
|
+
console.log('\nTo reinitialize, please remove the configuration first.');
|
|
111
96
|
process.exit(1);
|
|
112
97
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
if (servicesAdded) {
|
|
116
|
-
try {
|
|
117
|
-
await generateCaddyFile();
|
|
118
|
-
console.log('✨ Environment successfully prepared and anchored!');
|
|
119
|
-
}
|
|
120
|
-
catch (err) {
|
|
121
|
-
if (err instanceof Error && err.message.includes('No harbor configuration found')) {
|
|
122
|
-
// This is expected if no services were added
|
|
123
|
-
console.log('✨ Environment prepared! No services configured yet.');
|
|
124
|
-
}
|
|
125
|
-
else {
|
|
126
|
-
console.log('❌ Error generating Caddyfile:', err instanceof Error ? err.message : 'Unknown error');
|
|
127
|
-
process.exit(1);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
else {
|
|
132
|
-
console.log('✨ Environment prepared! No services configured yet.');
|
|
133
|
-
}
|
|
98
|
+
await generateDevFile(options.path);
|
|
99
|
+
console.log('✨ Environment prepared!');
|
|
134
100
|
}
|
|
135
101
|
catch (err) {
|
|
136
102
|
console.log('❌ Error:', err instanceof Error ? err.message : 'Unknown error');
|
|
@@ -149,19 +115,6 @@ program.command('moor')
|
|
|
149
115
|
}
|
|
150
116
|
await generateDevFile(options.path);
|
|
151
117
|
});
|
|
152
|
-
program.command('anchor')
|
|
153
|
-
.description(`Add new services to your Caddyfile
|
|
154
|
-
|
|
155
|
-
Note: This command will stop any active Caddy processes, including those from other Harbor projects.`)
|
|
156
|
-
.action(async () => {
|
|
157
|
-
if (!checkHasHarborConfig()) {
|
|
158
|
-
console.log('❌ No harbor configuration found');
|
|
159
|
-
console.log('\nTo initialize a new Harbor project, please use:');
|
|
160
|
-
console.log(' harbor dock');
|
|
161
|
-
process.exit(1);
|
|
162
|
-
}
|
|
163
|
-
await generateCaddyFile();
|
|
164
|
-
});
|
|
165
118
|
program.command('launch')
|
|
166
119
|
.description(`Launch your services in the harbor terminal multiplexer (Using tmux)
|
|
167
120
|
|
|
@@ -184,9 +137,6 @@ function isProjectDirectory(dirPath) {
|
|
|
184
137
|
});
|
|
185
138
|
}
|
|
186
139
|
function validateConfig(config) {
|
|
187
|
-
if (!config.domain) {
|
|
188
|
-
return 'Domain is required';
|
|
189
|
-
}
|
|
190
140
|
if (!Array.isArray(config.services)) {
|
|
191
141
|
return 'Services must be an array';
|
|
192
142
|
}
|
|
@@ -227,8 +177,6 @@ async function generateDevFile(dirPath) {
|
|
|
227
177
|
// If package.json exists but no harbor config, use it
|
|
228
178
|
writeToPackageJson = true;
|
|
229
179
|
config = {
|
|
230
|
-
domain: 'localhost',
|
|
231
|
-
useSudo: false,
|
|
232
180
|
services: [],
|
|
233
181
|
};
|
|
234
182
|
console.log('Creating new harbor config in package.json...');
|
|
@@ -236,8 +184,6 @@ async function generateDevFile(dirPath) {
|
|
|
236
184
|
else {
|
|
237
185
|
// No package.json, create harbor.json
|
|
238
186
|
config = {
|
|
239
|
-
domain: 'localhost',
|
|
240
|
-
useSudo: false,
|
|
241
187
|
services: [],
|
|
242
188
|
};
|
|
243
189
|
console.log('Creating new harbor.json...');
|
|
@@ -249,8 +195,6 @@ async function generateDevFile(dirPath) {
|
|
|
249
195
|
}
|
|
250
196
|
// No package.json, create harbor.json
|
|
251
197
|
config = {
|
|
252
|
-
domain: 'localhost',
|
|
253
|
-
useSudo: false,
|
|
254
198
|
services: [],
|
|
255
199
|
};
|
|
256
200
|
console.log('Creating new harbor.json...');
|
|
@@ -268,7 +212,6 @@ async function generateDevFile(dirPath) {
|
|
|
268
212
|
const service = {
|
|
269
213
|
name: folder.name,
|
|
270
214
|
path: folderPath,
|
|
271
|
-
subdomain: folder.name.toLowerCase().replace(/\s+/g, ''),
|
|
272
215
|
};
|
|
273
216
|
// Try to determine default command based on project type
|
|
274
217
|
if (fs.existsSync(path.join(folderPath, 'package.json'))) {
|
|
@@ -291,25 +234,6 @@ async function generateDevFile(dirPath) {
|
|
|
291
234
|
}
|
|
292
235
|
if (!newServicesAdded) {
|
|
293
236
|
console.log('No new services found to add, feel free to add them manually');
|
|
294
|
-
// Still write the initial config even if no services were found
|
|
295
|
-
const validationError = validateConfig(config);
|
|
296
|
-
if (validationError) {
|
|
297
|
-
throw new Error(`Invalid harbor configuration: ${validationError}`);
|
|
298
|
-
}
|
|
299
|
-
if (writeToPackageJson) {
|
|
300
|
-
// Update package.json
|
|
301
|
-
const packageData = await fs.promises.readFile('package.json', 'utf-8');
|
|
302
|
-
const packageJson = JSON.parse(packageData);
|
|
303
|
-
packageJson.harbor = config;
|
|
304
|
-
await fs.promises.writeFile('package.json', JSON.stringify(packageJson, null, 2), 'utf-8');
|
|
305
|
-
console.log('\npackage.json updated successfully with harbor configuration');
|
|
306
|
-
}
|
|
307
|
-
else {
|
|
308
|
-
// Write to harbor.json
|
|
309
|
-
await fs.promises.writeFile('harbor.json', JSON.stringify(config, null, 2), 'utf-8');
|
|
310
|
-
console.log('\nharbor.json created successfully');
|
|
311
|
-
}
|
|
312
|
-
return false;
|
|
313
237
|
}
|
|
314
238
|
const validationError = validateConfig(config);
|
|
315
239
|
if (validationError) {
|
|
@@ -329,7 +253,6 @@ async function generateDevFile(dirPath) {
|
|
|
329
253
|
console.log('\nharbor.json created successfully');
|
|
330
254
|
}
|
|
331
255
|
console.log('\nImportant:');
|
|
332
|
-
console.log(' - Update the \'Port\' field for each service to match its actual port or leave blank to ignore in the Caddyfile');
|
|
333
256
|
console.log(' - Verify the auto-detected commands are correct for your services');
|
|
334
257
|
return true;
|
|
335
258
|
}
|
|
@@ -373,60 +296,6 @@ async function readHarborConfig() {
|
|
|
373
296
|
}
|
|
374
297
|
throw new Error('No harbor configuration found in harbor.json or package.json');
|
|
375
298
|
}
|
|
376
|
-
async function stopCaddy() {
|
|
377
|
-
try {
|
|
378
|
-
console.log('\n⚠️ Stopping any existing Caddy processes...');
|
|
379
|
-
console.log(' This will interrupt any active Harbor or Caddy services\n');
|
|
380
|
-
// Try to kill any existing Caddy processes
|
|
381
|
-
await new Promise((resolve) => {
|
|
382
|
-
const isWindows = process.platform === 'win32';
|
|
383
|
-
const killCommand = isWindows ? 'taskkill /F /IM caddy.exe' : 'pkill caddy';
|
|
384
|
-
const childProcess = spawn('sh', ['-c', killCommand]);
|
|
385
|
-
childProcess.on('close', () => {
|
|
386
|
-
// It's okay if there was no process to kill (code 1)
|
|
387
|
-
resolve();
|
|
388
|
-
});
|
|
389
|
-
});
|
|
390
|
-
// Give it a moment to fully release ports
|
|
391
|
-
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
392
|
-
}
|
|
393
|
-
catch (err) {
|
|
394
|
-
// Ignore errors as the process might not exist
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
async function generateCaddyFile() {
|
|
398
|
-
try {
|
|
399
|
-
const config = await readHarborConfig();
|
|
400
|
-
let caddyfileContent = '';
|
|
401
|
-
// Check if any services need a Caddyfile
|
|
402
|
-
const needsCaddyfile = config.services.some(svc => svc.port && svc.subdomain);
|
|
403
|
-
if (!needsCaddyfile) {
|
|
404
|
-
// If no services need a Caddyfile, remove it if it exists
|
|
405
|
-
if (fileExists('Caddyfile')) {
|
|
406
|
-
await fs.promises.unlink('Caddyfile');
|
|
407
|
-
console.log('Removed Caddyfile as no services require subdomains');
|
|
408
|
-
}
|
|
409
|
-
return;
|
|
410
|
-
}
|
|
411
|
-
for (const svc of config.services) {
|
|
412
|
-
if (!svc.port || !svc.subdomain) {
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
const serverName = `${svc.subdomain}.${config.domain}`;
|
|
416
|
-
caddyfileContent += `${serverName} {\n`;
|
|
417
|
-
caddyfileContent += ` reverse_proxy localhost:${svc.port}\n`;
|
|
418
|
-
caddyfileContent += "}\n\n";
|
|
419
|
-
}
|
|
420
|
-
await fs.promises.writeFile('Caddyfile', caddyfileContent, 'utf-8');
|
|
421
|
-
// Stop existing Caddy process before proceeding
|
|
422
|
-
await stopCaddy();
|
|
423
|
-
console.log('Caddyfile generated successfully');
|
|
424
|
-
}
|
|
425
|
-
catch (err) {
|
|
426
|
-
console.error('Error generating Caddyfile:', err);
|
|
427
|
-
process.exit(1);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
299
|
async function runServices() {
|
|
431
300
|
const hasHarborConfig = checkHasHarborConfig();
|
|
432
301
|
if (!hasHarborConfig) {
|
|
@@ -449,18 +318,6 @@ async function runServices() {
|
|
|
449
318
|
console.error('Error reading config:', err);
|
|
450
319
|
process.exit(1);
|
|
451
320
|
}
|
|
452
|
-
// Check if any services need a Caddyfile
|
|
453
|
-
const needsCaddyfile = config.services.some(svc => svc.port && svc.subdomain);
|
|
454
|
-
if (needsCaddyfile && !fileExists('Caddyfile')) {
|
|
455
|
-
console.log('❌ No Caddyfile found, but some services require subdomains');
|
|
456
|
-
console.log('\nTo generate the Caddyfile:');
|
|
457
|
-
console.log(' harbor anchor');
|
|
458
|
-
process.exit(1);
|
|
459
|
-
}
|
|
460
|
-
// Stop any existing Caddy process if we need it
|
|
461
|
-
if (needsCaddyfile) {
|
|
462
|
-
await stopCaddy();
|
|
463
|
-
}
|
|
464
321
|
// Ensure scripts exist and are executable
|
|
465
322
|
await ensureScriptsExist();
|
|
466
323
|
// Execute the script directly using spawn to handle I/O streams
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abyrd9/harbor-cli",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "A CLI tool for managing local development services with
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "A CLI tool for managing simple local development services with tmux sessions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"harbor": "dist/index.js"
|
|
@@ -21,8 +21,6 @@
|
|
|
21
21
|
"keywords": [
|
|
22
22
|
"cli",
|
|
23
23
|
"development",
|
|
24
|
-
"proxy",
|
|
25
|
-
"caddy",
|
|
26
24
|
"tmux",
|
|
27
25
|
"local-development"
|
|
28
26
|
],
|
|
@@ -40,31 +38,25 @@
|
|
|
40
38
|
"node": ">=18.0.0"
|
|
41
39
|
},
|
|
42
40
|
"dependencies": {
|
|
43
|
-
"@commander-js/extra-typings": "^
|
|
41
|
+
"@commander-js/extra-typings": "^14.0.0"
|
|
44
42
|
},
|
|
45
43
|
"devDependencies": {
|
|
46
|
-
"@types/node": "^
|
|
44
|
+
"@types/node": "^24.0.3",
|
|
47
45
|
"bun-types": "latest",
|
|
48
|
-
"typescript": "^5.
|
|
46
|
+
"typescript": "^5.8.3"
|
|
49
47
|
},
|
|
50
48
|
"main": "index.js",
|
|
51
49
|
"harbor": {
|
|
52
|
-
"domain": "localhost",
|
|
53
|
-
"useSudo": true,
|
|
54
50
|
"services": [
|
|
55
51
|
{
|
|
56
52
|
"name": "vite-project",
|
|
57
53
|
"path": "test-services/vite-project",
|
|
58
|
-
"
|
|
59
|
-
"command": "npm run dev",
|
|
60
|
-
"port": 5173
|
|
54
|
+
"command": "npm run dev"
|
|
61
55
|
},
|
|
62
56
|
{
|
|
63
57
|
"name": "go-api",
|
|
64
58
|
"path": "test-services/go-api",
|
|
65
|
-
"
|
|
66
|
-
"command": "go run .",
|
|
67
|
-
"port": 8080
|
|
59
|
+
"command": "go run ."
|
|
68
60
|
}
|
|
69
61
|
]
|
|
70
62
|
}
|
package/scripts/dev.sh
CHANGED
|
@@ -79,36 +79,8 @@ tmux set-option -g 'status-format[0]' ''
|
|
|
79
79
|
echo "Creating window for interactive shell"
|
|
80
80
|
tmux rename-window -t local-dev-test:0 'Terminal'
|
|
81
81
|
|
|
82
|
-
# Check if any services need Caddy
|
|
83
|
-
needs_caddy=false
|
|
84
|
-
echo "Checking for services that need Caddy..."
|
|
85
|
-
if get_harbor_config | jq -e '.services[] | select(.subdomain != null and .port != null) | true' > /dev/null; then
|
|
86
|
-
needs_caddy=true
|
|
87
|
-
echo "Found services that need Caddy"
|
|
88
|
-
fi
|
|
89
|
-
echo "needs_caddy value: $needs_caddy"
|
|
90
|
-
|
|
91
82
|
window_index=1 # Start from index 1
|
|
92
83
|
|
|
93
|
-
# Create a new window for Caddy if needed
|
|
94
|
-
if [ "$needs_caddy" = true ]; then
|
|
95
|
-
echo "Creating window for Caddy"
|
|
96
|
-
tmux new-window -t local-dev-test:1 -n 'Caddy'
|
|
97
|
-
|
|
98
|
-
# Check if useSudo is true in harbor config
|
|
99
|
-
use_sudo=$(get_harbor_config | jq -r '.useSudo // false')
|
|
100
|
-
echo "useSudo value: $use_sudo"
|
|
101
|
-
if [ "$use_sudo" = "true" ]; then
|
|
102
|
-
echo "Running Caddy with sudo"
|
|
103
|
-
tmux send-keys -t local-dev-test:1 'sudo caddy run' C-m
|
|
104
|
-
else
|
|
105
|
-
echo "Running Caddy without sudo"
|
|
106
|
-
tmux send-keys -t local-dev-test:1 'caddy run' C-m
|
|
107
|
-
fi
|
|
108
|
-
|
|
109
|
-
window_index=2 # Start services from index 2
|
|
110
|
-
fi
|
|
111
|
-
|
|
112
84
|
# Create windows dynamically based on harbor config
|
|
113
85
|
get_harbor_config | jq -c '.services[]' | while read service; do
|
|
114
86
|
name=$(echo $service | jq -r '.name')
|