moltengine-cli 0.1.0 → 0.1.2
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 +11 -11
- package/package.json +9 -4
- package/src/__tests__/utils.test.js +218 -0
- package/src/moltengine.js +148 -68
- package/src/utils.js +148 -0
package/README.md
CHANGED
|
@@ -43,26 +43,26 @@ source ~/.bashrc
|
|
|
43
43
|
|
|
44
44
|
```bash
|
|
45
45
|
# Show tenant information
|
|
46
|
-
|
|
46
|
+
moltctl whoami
|
|
47
47
|
|
|
48
48
|
# Check status
|
|
49
|
-
|
|
49
|
+
moltctl status
|
|
50
50
|
|
|
51
51
|
# List deployments
|
|
52
|
-
|
|
52
|
+
moltctl deployments
|
|
53
53
|
|
|
54
54
|
# Show help
|
|
55
|
-
|
|
55
|
+
moltctl help
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
## Commands
|
|
59
59
|
|
|
60
60
|
| Command | Description |
|
|
61
61
|
|---------|-------------|
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
62
|
+
| `moltctl whoami` | Show current tenant information |
|
|
63
|
+
| `moltctl status` | Show tenant status and deployment info |
|
|
64
|
+
| `moltctl deployments` | List recent deployments |
|
|
65
|
+
| `moltctl help` | Show help message |
|
|
66
66
|
|
|
67
67
|
## Environment Variables
|
|
68
68
|
|
|
@@ -75,7 +75,7 @@ moltengine help
|
|
|
75
75
|
|
|
76
76
|
Your API key is displayed on the success page after completing checkout at [moltengine.com](https://moltengine.com).
|
|
77
77
|
|
|
78
|
-
If you've lost your API key, contact support@moltengine.com.
|
|
78
|
+
If you've lost your API key, contact support@mg.moltengine.com.
|
|
79
79
|
|
|
80
80
|
## Development
|
|
81
81
|
|
|
@@ -92,7 +92,7 @@ npm start -- status
|
|
|
92
92
|
|
|
93
93
|
# Link for local development
|
|
94
94
|
npm link
|
|
95
|
-
|
|
95
|
+
moltctl status
|
|
96
96
|
```
|
|
97
97
|
|
|
98
98
|
## Publishing (Maintainers)
|
|
@@ -115,5 +115,5 @@ MIT
|
|
|
115
115
|
|
|
116
116
|
## Support
|
|
117
117
|
|
|
118
|
-
- Email: support@moltengine.com
|
|
118
|
+
- Email: support@mg.moltengine.com
|
|
119
119
|
- Docs: https://docs.moltengine.com
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moltengine-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "CLI for Moltengine - The WP Engine for AI Agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,20 +22,25 @@
|
|
|
22
22
|
"cli"
|
|
23
23
|
],
|
|
24
24
|
"bin": {
|
|
25
|
-
"
|
|
25
|
+
"moltctl": "src/moltengine.js"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
28
|
"src/**/*"
|
|
29
29
|
],
|
|
30
30
|
"scripts": {
|
|
31
31
|
"start": "node src/moltengine.js",
|
|
32
|
-
"test": "
|
|
33
|
-
"
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest",
|
|
34
|
+
"test:help": "node src/moltengine.js --help",
|
|
35
|
+
"prepublishOnly": "npm run test:help"
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
36
38
|
"undici": "^6.21.0"
|
|
37
39
|
},
|
|
38
40
|
"engines": {
|
|
39
41
|
"node": ">=20"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"vitest": "^4.0.18"
|
|
40
45
|
}
|
|
41
46
|
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
colors,
|
|
4
|
+
formatDate,
|
|
5
|
+
formatStatus,
|
|
6
|
+
stripColors,
|
|
7
|
+
isValidApiKey,
|
|
8
|
+
isValidUrl,
|
|
9
|
+
parseArgs
|
|
10
|
+
} from '../utils.js';
|
|
11
|
+
|
|
12
|
+
describe('CLI Utils', () => {
|
|
13
|
+
describe('colors', () => {
|
|
14
|
+
it('should have all required color codes', () => {
|
|
15
|
+
expect(colors.reset).toBeDefined();
|
|
16
|
+
expect(colors.bold).toBeDefined();
|
|
17
|
+
expect(colors.primary).toBeDefined();
|
|
18
|
+
expect(colors.accent).toBeDefined();
|
|
19
|
+
expect(colors.success).toBeDefined();
|
|
20
|
+
expect(colors.error).toBeDefined();
|
|
21
|
+
expect(colors.warning).toBeDefined();
|
|
22
|
+
expect(colors.info).toBeDefined();
|
|
23
|
+
expect(colors.muted).toBeDefined();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should have valid ANSI escape sequences', () => {
|
|
27
|
+
expect(colors.reset).toBe('\x1b[0m');
|
|
28
|
+
expect(colors.bold).toBe('\x1b[1m');
|
|
29
|
+
expect(colors.success).toBe('\x1b[32m');
|
|
30
|
+
expect(colors.error).toBe('\x1b[31m');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('formatDate', () => {
|
|
35
|
+
it('should return N/A for null', () => {
|
|
36
|
+
expect(formatDate(null)).toBe('N/A');
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('should return N/A for undefined', () => {
|
|
40
|
+
expect(formatDate(undefined)).toBe('N/A');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should return N/A for empty string', () => {
|
|
44
|
+
expect(formatDate('')).toBe('N/A');
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should format valid ISO date string', () => {
|
|
48
|
+
const result = formatDate('2024-01-15T12:30:00Z');
|
|
49
|
+
// Result depends on locale, but should contain date parts
|
|
50
|
+
expect(result).not.toBe('N/A');
|
|
51
|
+
expect(result.length).toBeGreaterThan(5);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should handle date-only strings', () => {
|
|
55
|
+
const result = formatDate('2024-01-15');
|
|
56
|
+
expect(result).not.toBe('N/A');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('formatStatus', () => {
|
|
61
|
+
it('should color active status green', () => {
|
|
62
|
+
const result = formatStatus('active');
|
|
63
|
+
expect(result).toContain(colors.success);
|
|
64
|
+
expect(result).toContain('active');
|
|
65
|
+
expect(result).toContain(colors.reset);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should color pending status yellow', () => {
|
|
69
|
+
const result = formatStatus('pending');
|
|
70
|
+
expect(result).toContain(colors.warning);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should color failed status red', () => {
|
|
74
|
+
const result = formatStatus('failed');
|
|
75
|
+
expect(result).toContain(colors.error);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('should color running status accent', () => {
|
|
79
|
+
const result = formatStatus('running');
|
|
80
|
+
expect(result).toContain(colors.accent);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should handle case-insensitive status', () => {
|
|
84
|
+
const result = formatStatus('ACTIVE');
|
|
85
|
+
expect(result).toContain(colors.success);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle null status', () => {
|
|
89
|
+
const result = formatStatus(null);
|
|
90
|
+
expect(result).toContain(colors.reset);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('should handle unknown status', () => {
|
|
94
|
+
const result = formatStatus('unknown-status');
|
|
95
|
+
expect(result).toContain('unknown-status');
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
describe('stripColors', () => {
|
|
100
|
+
it('should remove ANSI color codes', () => {
|
|
101
|
+
const colored = `${colors.success}Hello${colors.reset}`;
|
|
102
|
+
expect(stripColors(colored)).toBe('Hello');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should handle multiple color codes', () => {
|
|
106
|
+
const colored = `${colors.bold}${colors.primary}Test${colors.reset}`;
|
|
107
|
+
expect(stripColors(colored)).toBe('Test');
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('should return plain string unchanged', () => {
|
|
111
|
+
expect(stripColors('Hello World')).toBe('Hello World');
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should handle empty string', () => {
|
|
115
|
+
expect(stripColors('')).toBe('');
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('isValidApiKey', () => {
|
|
120
|
+
it('should return true for valid 48-char hex key', () => {
|
|
121
|
+
const validKey = 'a'.repeat(48);
|
|
122
|
+
expect(isValidApiKey(validKey)).toBe(true);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should return true for mixed case hex', () => {
|
|
126
|
+
const validKey = 'aAbBcCdDeEfF'.repeat(4);
|
|
127
|
+
expect(isValidApiKey(validKey)).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should return false for too short key', () => {
|
|
131
|
+
expect(isValidApiKey('abc123')).toBe(false);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should return false for too long key', () => {
|
|
135
|
+
expect(isValidApiKey('a'.repeat(50))).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('should return false for non-hex characters', () => {
|
|
139
|
+
const invalidKey = 'g'.repeat(48);
|
|
140
|
+
expect(isValidApiKey(invalidKey)).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should return false for null', () => {
|
|
144
|
+
expect(isValidApiKey(null)).toBe(false);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should return false for undefined', () => {
|
|
148
|
+
expect(isValidApiKey(undefined)).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return false for empty string', () => {
|
|
152
|
+
expect(isValidApiKey('')).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
describe('isValidUrl', () => {
|
|
157
|
+
it('should return true for valid http URL', () => {
|
|
158
|
+
expect(isValidUrl('http://localhost:8080')).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should return true for valid https URL', () => {
|
|
162
|
+
expect(isValidUrl('https://api.moltengine.com')).toBe(true);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should return true for URL with path', () => {
|
|
166
|
+
expect(isValidUrl('https://api.moltengine.com/v1/tenants')).toBe(true);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should return false for invalid URL', () => {
|
|
170
|
+
expect(isValidUrl('not-a-url')).toBe(false);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should return false for empty string', () => {
|
|
174
|
+
expect(isValidUrl('')).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe('parseArgs', () => {
|
|
179
|
+
it('should parse command from argv', () => {
|
|
180
|
+
const result = parseArgs(['node', 'moltengine.js', 'status']);
|
|
181
|
+
expect(result.command).toBe('status');
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it('should default to help when no command', () => {
|
|
185
|
+
const result = parseArgs(['node', 'moltengine.js']);
|
|
186
|
+
expect(result.command).toBe('help');
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('should parse positional arguments', () => {
|
|
190
|
+
const result = parseArgs(['node', 'moltengine.js', 'deploy', 'app1', 'app2']);
|
|
191
|
+
expect(result.command).toBe('deploy');
|
|
192
|
+
expect(result.args).toEqual(['app1', 'app2']);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('should parse long flags', () => {
|
|
196
|
+
const result = parseArgs(['node', 'moltengine.js', 'status', '--verbose']);
|
|
197
|
+
expect(result.flags.verbose).toBe(true);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it('should parse long flags with values', () => {
|
|
201
|
+
const result = parseArgs(['node', 'moltengine.js', 'deploy', '--env=production']);
|
|
202
|
+
expect(result.flags.env).toBe('production');
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('should parse short flags', () => {
|
|
206
|
+
const result = parseArgs(['node', 'moltengine.js', 'status', '-v']);
|
|
207
|
+
expect(result.flags.v).toBe(true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should handle mixed args and flags', () => {
|
|
211
|
+
const result = parseArgs(['node', 'moltengine.js', 'deploy', 'app1', '--force', '-v']);
|
|
212
|
+
expect(result.command).toBe('deploy');
|
|
213
|
+
expect(result.args).toEqual(['app1']);
|
|
214
|
+
expect(result.flags.force).toBe(true);
|
|
215
|
+
expect(result.flags.v).toBe(true);
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|
package/src/moltengine.js
CHANGED
|
@@ -6,11 +6,30 @@ import { request } from "undici";
|
|
|
6
6
|
const API_URL = process.env.MOLTENGINE_API || "http://localhost:8080";
|
|
7
7
|
const TENANT_KEY = process.env.MOLT_TENANT_KEY || "";
|
|
8
8
|
|
|
9
|
-
//
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// BRAND COLORS (ANSI 24-bit RGB)
|
|
11
|
+
// Derived from elegant-luxury theme
|
|
12
|
+
// ============================================================================
|
|
13
|
+
|
|
10
14
|
const colors = {
|
|
11
15
|
reset: "\x1b[0m",
|
|
12
16
|
bold: "\x1b[1m",
|
|
13
17
|
dim: "\x1b[2m",
|
|
18
|
+
italic: "\x1b[3m",
|
|
19
|
+
underline: "\x1b[4m",
|
|
20
|
+
|
|
21
|
+
// Brand colors (24-bit RGB)
|
|
22
|
+
primary: "\x1b[38;2;194;65;12m", // #c2410c - terracotta
|
|
23
|
+
accent: "\x1b[38;2;234;88;12m", // #ea580c - bright orange
|
|
24
|
+
|
|
25
|
+
// Semantic colors
|
|
26
|
+
success: "\x1b[32m", // Green
|
|
27
|
+
warning: "\x1b[33m", // Yellow
|
|
28
|
+
error: "\x1b[31m", // Red
|
|
29
|
+
info: "\x1b[36m", // Cyan
|
|
30
|
+
muted: "\x1b[90m", // Gray
|
|
31
|
+
|
|
32
|
+
// Legacy aliases
|
|
14
33
|
green: "\x1b[32m",
|
|
15
34
|
yellow: "\x1b[33m",
|
|
16
35
|
red: "\x1b[31m",
|
|
@@ -18,6 +37,29 @@ const colors = {
|
|
|
18
37
|
blue: "\x1b[34m"
|
|
19
38
|
};
|
|
20
39
|
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// ASCII ART LOGO
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
const ASCII_LOGO = `
|
|
45
|
+
${colors.primary} __ __ _ _ _
|
|
46
|
+
| \\/ | ___ | | |_ ___ _ __ __ _(_)_ __ ___
|
|
47
|
+
| |\\/| |/ _ \\| | __/ _ \\ '_ \\ / _\` | | '_ \\ / _ \\
|
|
48
|
+
| | | | (_) | | || __/ | | | (_| | | | | | __/
|
|
49
|
+
|_| |_|\\___/|_|\\__\\___|_| |_|\\__, |_|_| |_|\\___|
|
|
50
|
+
|___/ ${colors.reset}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const ASCII_LOGO_COMPACT = `
|
|
54
|
+
${colors.primary} ╔╦╗╔═╗╦ ╔╦╗╔═╗╔╗╔╔═╗╦╔╗╔╔═╗
|
|
55
|
+
║║║║ ║║ ║ ║╣ ║║║║ ╦║║║║║╣
|
|
56
|
+
╩ ╩╚═╝╩═╝╩ ╚═╝╝╚╝╚═╝╩╝╚╝╚═╝${colors.reset}
|
|
57
|
+
`;
|
|
58
|
+
|
|
59
|
+
// ============================================================================
|
|
60
|
+
// OUTPUT HELPERS
|
|
61
|
+
// ============================================================================
|
|
62
|
+
|
|
21
63
|
/**
|
|
22
64
|
* Print colored output
|
|
23
65
|
*/
|
|
@@ -25,14 +67,41 @@ function print(message, color = "") {
|
|
|
25
67
|
console.log(color ? `${color}${message}${colors.reset}` : message);
|
|
26
68
|
}
|
|
27
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Print branded header
|
|
72
|
+
*/
|
|
73
|
+
function printHeader(title) {
|
|
74
|
+
console.log();
|
|
75
|
+
print(title, colors.bold);
|
|
76
|
+
print("─".repeat(40), colors.muted);
|
|
77
|
+
}
|
|
78
|
+
|
|
28
79
|
/**
|
|
29
80
|
* Print error and exit
|
|
30
81
|
*/
|
|
31
82
|
function error(message) {
|
|
32
|
-
print(
|
|
83
|
+
print(`✗ Error: ${message}`, colors.error);
|
|
33
84
|
process.exit(1);
|
|
34
85
|
}
|
|
35
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Print success message
|
|
89
|
+
*/
|
|
90
|
+
function success(message) {
|
|
91
|
+
print(`✓ ${message}`, colors.success);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Print info message
|
|
96
|
+
*/
|
|
97
|
+
function info(message) {
|
|
98
|
+
print(`→ ${message}`, colors.info);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// API CLIENT
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
36
105
|
/**
|
|
37
106
|
* Make an authenticated GET request
|
|
38
107
|
*/
|
|
@@ -68,6 +137,10 @@ async function apiGet(path) {
|
|
|
68
137
|
}
|
|
69
138
|
}
|
|
70
139
|
|
|
140
|
+
// ============================================================================
|
|
141
|
+
// FORMATTERS
|
|
142
|
+
// ============================================================================
|
|
143
|
+
|
|
71
144
|
/**
|
|
72
145
|
* Format a date string
|
|
73
146
|
*/
|
|
@@ -81,16 +154,16 @@ function formatDate(dateStr) {
|
|
|
81
154
|
*/
|
|
82
155
|
function formatStatus(status) {
|
|
83
156
|
const statusColors = {
|
|
84
|
-
active: colors.
|
|
85
|
-
pending: colors.
|
|
86
|
-
inactive: colors.
|
|
87
|
-
past_due: colors.
|
|
88
|
-
canceled: colors.
|
|
89
|
-
suspended: colors.
|
|
90
|
-
succeeded: colors.
|
|
91
|
-
running: colors.
|
|
92
|
-
queued: colors.
|
|
93
|
-
failed: colors.
|
|
157
|
+
active: colors.success,
|
|
158
|
+
pending: colors.warning,
|
|
159
|
+
inactive: colors.muted,
|
|
160
|
+
past_due: colors.error,
|
|
161
|
+
canceled: colors.error,
|
|
162
|
+
suspended: colors.error,
|
|
163
|
+
succeeded: colors.success,
|
|
164
|
+
running: colors.accent,
|
|
165
|
+
queued: colors.warning,
|
|
166
|
+
failed: colors.error
|
|
94
167
|
};
|
|
95
168
|
|
|
96
169
|
const color = statusColors[status?.toLowerCase()] || colors.reset;
|
|
@@ -102,36 +175,40 @@ function formatStatus(status) {
|
|
|
102
175
|
// ============================================================================
|
|
103
176
|
|
|
104
177
|
/**
|
|
105
|
-
* Display help
|
|
178
|
+
* Display help with branding
|
|
106
179
|
*/
|
|
107
180
|
function showHelp() {
|
|
108
|
-
console.log(
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
${colors.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
${colors.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
#
|
|
130
|
-
moltengine
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
`);
|
|
181
|
+
console.log(ASCII_LOGO);
|
|
182
|
+
console.log(`${colors.muted}Secure Clawd. Without Fear.${colors.reset}`);
|
|
183
|
+
console.log();
|
|
184
|
+
|
|
185
|
+
print("USAGE", colors.bold);
|
|
186
|
+
console.log(` ${colors.primary}moltctl${colors.reset} <command> [options]`);
|
|
187
|
+
console.log();
|
|
188
|
+
|
|
189
|
+
print("COMMANDS", colors.bold);
|
|
190
|
+
console.log(` ${colors.accent}whoami${colors.reset} Show current tenant information`);
|
|
191
|
+
console.log(` ${colors.accent}status${colors.reset} Show tenant status and deployment info`);
|
|
192
|
+
console.log(` ${colors.accent}deployments${colors.reset} List recent deployments`);
|
|
193
|
+
console.log(` ${colors.accent}help${colors.reset} Show this help message`);
|
|
194
|
+
console.log();
|
|
195
|
+
|
|
196
|
+
print("ENVIRONMENT", colors.bold);
|
|
197
|
+
console.log(` ${colors.muted}MOLTENGINE_API${colors.reset} API URL (default: http://localhost:8080)`);
|
|
198
|
+
console.log(` ${colors.muted}MOLT_TENANT_KEY${colors.reset} Your tenant API key (required)`);
|
|
199
|
+
console.log();
|
|
200
|
+
|
|
201
|
+
print("EXAMPLES", colors.bold);
|
|
202
|
+
console.log(` ${colors.muted}# Set up environment${colors.reset}`);
|
|
203
|
+
console.log(` export MOLTENGINE_API="https://api.moltengine.com"`);
|
|
204
|
+
console.log(` export MOLT_TENANT_KEY="your-tenant-key-here"`);
|
|
205
|
+
console.log();
|
|
206
|
+
console.log(` ${colors.muted}# Check your tenant info${colors.reset}`);
|
|
207
|
+
console.log(` ${colors.primary}moltctl${colors.reset} whoami`);
|
|
208
|
+
console.log();
|
|
209
|
+
console.log(` ${colors.muted}# Check status${colors.reset}`);
|
|
210
|
+
console.log(` ${colors.primary}moltctl${colors.reset} status`);
|
|
211
|
+
console.log();
|
|
135
212
|
}
|
|
136
213
|
|
|
137
214
|
/**
|
|
@@ -140,16 +217,14 @@ ${colors.bold}EXAMPLES${colors.reset}
|
|
|
140
217
|
async function cmdWhoami() {
|
|
141
218
|
const data = await apiGet("/tenant/me");
|
|
142
219
|
|
|
143
|
-
|
|
144
|
-
${colors.
|
|
145
|
-
${
|
|
146
|
-
${colors.
|
|
147
|
-
${colors.
|
|
148
|
-
${colors.
|
|
149
|
-
${colors.
|
|
150
|
-
|
|
151
|
-
${colors.dim}Created:${colors.reset} ${formatDate(data.createdAt)}
|
|
152
|
-
`);
|
|
220
|
+
printHeader("Tenant Information");
|
|
221
|
+
console.log(` ${colors.muted}ID:${colors.reset} ${data.tenantId}`);
|
|
222
|
+
console.log(` ${colors.muted}Name:${colors.reset} ${data.name}`);
|
|
223
|
+
console.log(` ${colors.muted}Email:${colors.reset} ${data.email}`);
|
|
224
|
+
console.log(` ${colors.muted}Status:${colors.reset} ${formatStatus(data.status)}`);
|
|
225
|
+
console.log(` ${colors.muted}FQDN:${colors.reset} ${data.fqdn || "(not configured)"}`);
|
|
226
|
+
console.log(` ${colors.muted}Created:${colors.reset} ${formatDate(data.createdAt)}`);
|
|
227
|
+
console.log();
|
|
153
228
|
}
|
|
154
229
|
|
|
155
230
|
/**
|
|
@@ -158,36 +233,37 @@ ${"─".repeat(40)}
|
|
|
158
233
|
async function cmdStatus() {
|
|
159
234
|
const data = await apiGet("/tenant/me");
|
|
160
235
|
|
|
161
|
-
|
|
162
|
-
|
|
236
|
+
console.log(ASCII_LOGO_COMPACT);
|
|
237
|
+
printHeader("Runtime Status");
|
|
163
238
|
|
|
164
239
|
// Status line
|
|
165
240
|
const statusIcon = data.status === "active" ? "●" : "○";
|
|
166
|
-
const statusColor = data.status === "active" ? colors.
|
|
167
|
-
|
|
241
|
+
const statusColor = data.status === "active" ? colors.success : colors.warning;
|
|
242
|
+
console.log(` ${colors.muted}Status:${colors.reset} ${statusColor}${statusIcon} ${data.status}${colors.reset}`);
|
|
168
243
|
|
|
169
244
|
// FQDN
|
|
170
245
|
if (data.fqdn) {
|
|
171
|
-
|
|
246
|
+
console.log(` ${colors.muted}URL:${colors.reset} ${colors.accent}https://${data.fqdn}${colors.reset}`);
|
|
172
247
|
} else {
|
|
173
|
-
|
|
248
|
+
console.log(` ${colors.muted}URL:${colors.reset} ${colors.muted}(provisioning...)${colors.reset}`);
|
|
174
249
|
}
|
|
175
250
|
|
|
176
251
|
// Last provisioned
|
|
177
|
-
|
|
252
|
+
console.log(` ${colors.muted}Deployed:${colors.reset} ${formatDate(data.lastProvisioned)}`);
|
|
178
253
|
|
|
179
254
|
// Error (if any)
|
|
180
255
|
if (data.lastError) {
|
|
181
|
-
|
|
256
|
+
console.log(` ${colors.error}Error: ${data.lastError}${colors.reset}`);
|
|
182
257
|
}
|
|
183
258
|
|
|
184
259
|
console.log();
|
|
185
260
|
|
|
186
261
|
// Show connection info if active
|
|
187
262
|
if (data.status === "active" && data.fqdn) {
|
|
188
|
-
print(
|
|
189
|
-
print(
|
|
190
|
-
|
|
263
|
+
print("Connection Info", colors.bold);
|
|
264
|
+
print("─".repeat(40), colors.muted);
|
|
265
|
+
console.log(` ${colors.muted}Dashboard:${colors.reset} ${colors.accent}https://${data.fqdn}${colors.reset}`);
|
|
266
|
+
console.log(` ${colors.muted}Gateway:${colors.reset} ${colors.accent}https://${data.fqdn}:18789${colors.reset}`);
|
|
191
267
|
console.log();
|
|
192
268
|
}
|
|
193
269
|
}
|
|
@@ -198,11 +274,10 @@ async function cmdStatus() {
|
|
|
198
274
|
async function cmdDeployments() {
|
|
199
275
|
const data = await apiGet("/tenant/deployments");
|
|
200
276
|
|
|
201
|
-
|
|
202
|
-
print("─".repeat(60));
|
|
277
|
+
printHeader("Recent Deployments");
|
|
203
278
|
|
|
204
279
|
if (!data.deployments || data.deployments.length === 0) {
|
|
205
|
-
|
|
280
|
+
console.log(` ${colors.muted}No deployments found${colors.reset}`);
|
|
206
281
|
console.log();
|
|
207
282
|
return;
|
|
208
283
|
}
|
|
@@ -210,9 +285,9 @@ async function cmdDeployments() {
|
|
|
210
285
|
for (const dep of data.deployments) {
|
|
211
286
|
const date = formatDate(dep.created_at);
|
|
212
287
|
const status = formatStatus(dep.status);
|
|
213
|
-
|
|
288
|
+
console.log(` ${colors.muted}[${date}]${colors.reset} ${dep.kind} - ${status}`);
|
|
214
289
|
if (dep.error) {
|
|
215
|
-
|
|
290
|
+
console.log(` ${colors.error}Error: ${dep.error.substring(0, 60)}...${colors.reset}`);
|
|
216
291
|
}
|
|
217
292
|
}
|
|
218
293
|
|
|
@@ -231,6 +306,11 @@ async function main() {
|
|
|
231
306
|
process.exit(0);
|
|
232
307
|
}
|
|
233
308
|
|
|
309
|
+
// Show compact logo for commands
|
|
310
|
+
if (command !== "help") {
|
|
311
|
+
console.log();
|
|
312
|
+
}
|
|
313
|
+
|
|
234
314
|
switch (command) {
|
|
235
315
|
case "whoami":
|
|
236
316
|
await cmdWhoami();
|
|
@@ -245,8 +325,8 @@ async function main() {
|
|
|
245
325
|
break;
|
|
246
326
|
|
|
247
327
|
default:
|
|
248
|
-
print(`Unknown command: ${command}`, colors.
|
|
249
|
-
print(`Run '
|
|
328
|
+
print(`Unknown command: ${command}`, colors.error);
|
|
329
|
+
print(`Run 'moltctl help' for usage information`, colors.muted);
|
|
250
330
|
process.exit(1);
|
|
251
331
|
}
|
|
252
332
|
}
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI Utility Functions
|
|
3
|
+
* Extracted for testability
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ============================================================================
|
|
7
|
+
// BRAND COLORS (ANSI 24-bit RGB)
|
|
8
|
+
// ============================================================================
|
|
9
|
+
|
|
10
|
+
export const colors = {
|
|
11
|
+
reset: "\x1b[0m",
|
|
12
|
+
bold: "\x1b[1m",
|
|
13
|
+
dim: "\x1b[2m",
|
|
14
|
+
italic: "\x1b[3m",
|
|
15
|
+
underline: "\x1b[4m",
|
|
16
|
+
|
|
17
|
+
// Brand colors (24-bit RGB)
|
|
18
|
+
primary: "\x1b[38;2;194;65;12m", // #c2410c - terracotta
|
|
19
|
+
accent: "\x1b[38;2;234;88;12m", // #ea580c - bright orange
|
|
20
|
+
|
|
21
|
+
// Semantic colors
|
|
22
|
+
success: "\x1b[32m", // Green
|
|
23
|
+
warning: "\x1b[33m", // Yellow
|
|
24
|
+
error: "\x1b[31m", // Red
|
|
25
|
+
info: "\x1b[36m", // Cyan
|
|
26
|
+
muted: "\x1b[90m", // Gray
|
|
27
|
+
|
|
28
|
+
// Legacy aliases
|
|
29
|
+
green: "\x1b[32m",
|
|
30
|
+
yellow: "\x1b[33m",
|
|
31
|
+
red: "\x1b[31m",
|
|
32
|
+
cyan: "\x1b[36m",
|
|
33
|
+
blue: "\x1b[34m"
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// FORMATTERS
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Format a date string for display
|
|
42
|
+
* @param {string|null|undefined} dateStr - ISO date string
|
|
43
|
+
* @returns {string} Formatted date or "N/A"
|
|
44
|
+
*/
|
|
45
|
+
export function formatDate(dateStr) {
|
|
46
|
+
if (!dateStr) return "N/A";
|
|
47
|
+
return new Date(dateStr).toLocaleString();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Format status with color
|
|
52
|
+
* @param {string|null|undefined} status - Status string
|
|
53
|
+
* @returns {string} Colored status string
|
|
54
|
+
*/
|
|
55
|
+
export function formatStatus(status) {
|
|
56
|
+
const statusColors = {
|
|
57
|
+
active: colors.success,
|
|
58
|
+
pending: colors.warning,
|
|
59
|
+
inactive: colors.muted,
|
|
60
|
+
past_due: colors.error,
|
|
61
|
+
canceled: colors.error,
|
|
62
|
+
suspended: colors.error,
|
|
63
|
+
succeeded: colors.success,
|
|
64
|
+
running: colors.accent,
|
|
65
|
+
queued: colors.warning,
|
|
66
|
+
failed: colors.error
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const color = statusColors[status?.toLowerCase()] || colors.reset;
|
|
70
|
+
return `${color}${status}${colors.reset}`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Strip ANSI color codes from a string
|
|
75
|
+
* @param {string} str - String with potential ANSI codes
|
|
76
|
+
* @returns {string} Plain string without colors
|
|
77
|
+
*/
|
|
78
|
+
export function stripColors(str) {
|
|
79
|
+
// eslint-disable-next-line no-control-regex
|
|
80
|
+
return str.replace(/\x1b\[[0-9;]*m/g, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ============================================================================
|
|
84
|
+
// VALIDATORS
|
|
85
|
+
// ============================================================================
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Check if a string looks like a valid API key
|
|
89
|
+
* @param {string} key - Potential API key
|
|
90
|
+
* @returns {boolean}
|
|
91
|
+
*/
|
|
92
|
+
export function isValidApiKey(key) {
|
|
93
|
+
if (!key || typeof key !== 'string') return false;
|
|
94
|
+
// Moltengine API keys are 48 character hex strings
|
|
95
|
+
return /^[a-f0-9]{48}$/i.test(key);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if a URL is valid
|
|
100
|
+
* @param {string} url - URL to validate
|
|
101
|
+
* @returns {boolean}
|
|
102
|
+
*/
|
|
103
|
+
export function isValidUrl(url) {
|
|
104
|
+
try {
|
|
105
|
+
new URL(url);
|
|
106
|
+
return true;
|
|
107
|
+
} catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// PARSERS
|
|
114
|
+
// ============================================================================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Parse command line arguments
|
|
118
|
+
* @param {string[]} argv - process.argv
|
|
119
|
+
* @returns {{ command: string, args: string[], flags: object }}
|
|
120
|
+
*/
|
|
121
|
+
export function parseArgs(argv) {
|
|
122
|
+
const [, , command, ...rest] = argv;
|
|
123
|
+
const args = [];
|
|
124
|
+
const flags = {};
|
|
125
|
+
|
|
126
|
+
for (const arg of rest) {
|
|
127
|
+
if (arg.startsWith('--')) {
|
|
128
|
+
const [key, value] = arg.slice(2).split('=');
|
|
129
|
+
flags[key] = value ?? true;
|
|
130
|
+
} else if (arg.startsWith('-')) {
|
|
131
|
+
flags[arg.slice(1)] = true;
|
|
132
|
+
} else {
|
|
133
|
+
args.push(arg);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return { command: command || 'help', args, flags };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export default {
|
|
141
|
+
colors,
|
|
142
|
+
formatDate,
|
|
143
|
+
formatStatus,
|
|
144
|
+
stripColors,
|
|
145
|
+
isValidApiKey,
|
|
146
|
+
isValidUrl,
|
|
147
|
+
parseArgs
|
|
148
|
+
};
|