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 CHANGED
@@ -43,26 +43,26 @@ source ~/.bashrc
43
43
 
44
44
  ```bash
45
45
  # Show tenant information
46
- moltengine whoami
46
+ moltctl whoami
47
47
 
48
48
  # Check status
49
- moltengine status
49
+ moltctl status
50
50
 
51
51
  # List deployments
52
- moltengine deployments
52
+ moltctl deployments
53
53
 
54
54
  # Show help
55
- moltengine help
55
+ moltctl help
56
56
  ```
57
57
 
58
58
  ## Commands
59
59
 
60
60
  | Command | Description |
61
61
  |---------|-------------|
62
- | `moltengine whoami` | Show current tenant information |
63
- | `moltengine status` | Show tenant status and deployment info |
64
- | `moltengine deployments` | List recent deployments |
65
- | `moltengine help` | Show help message |
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
- moltengine status
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.0",
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
- "moltengine": "src/moltengine.js"
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": "node src/moltengine.js --help",
33
- "prepublishOnly": "npm test"
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
- // ANSI colors
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(`Error: ${message}`, colors.red);
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.green,
85
- pending: colors.yellow,
86
- inactive: colors.dim,
87
- past_due: colors.red,
88
- canceled: colors.red,
89
- suspended: colors.red,
90
- succeeded: colors.green,
91
- running: colors.cyan,
92
- queued: colors.yellow,
93
- failed: colors.red
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
- ${colors.bold}moltengine${colors.reset} - Moltengine CLI
110
-
111
- ${colors.bold}USAGE${colors.reset}
112
- moltengine <command> [options]
113
-
114
- ${colors.bold}COMMANDS${colors.reset}
115
- whoami Show current tenant information
116
- status Show tenant status and deployment info
117
- deployments List recent deployments
118
- help Show this help message
119
-
120
- ${colors.bold}ENVIRONMENT${colors.reset}
121
- MOLTENGINE_API API URL (default: http://localhost:8080)
122
- MOLT_TENANT_KEY Your tenant API key (required)
123
-
124
- ${colors.bold}EXAMPLES${colors.reset}
125
- # Set up environment
126
- export MOLTENGINE_API="https://api.moltengine.io"
127
- export MOLT_TENANT_KEY="your-tenant-key-here"
128
-
129
- # Check your tenant info
130
- moltengine whoami
131
-
132
- # Check status
133
- moltengine status
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
- console.log(`
144
- ${colors.bold}Tenant Information${colors.reset}
145
- ${"─".repeat(40)}
146
- ${colors.dim}ID:${colors.reset} ${data.tenantId}
147
- ${colors.dim}Name:${colors.reset} ${data.name}
148
- ${colors.dim}Email:${colors.reset} ${data.email}
149
- ${colors.dim}Status:${colors.reset} ${formatStatus(data.status)}
150
- ${colors.dim}FQDN:${colors.reset} ${data.fqdn || "(not configured)"}
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
- print(`\n${colors.bold}Moltengine Status${colors.reset}`);
162
- print("".repeat(40));
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.green : colors.yellow;
167
- print(` Status: ${statusColor}${statusIcon} ${data.status}${colors.reset}`);
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
- print(` URL: ${colors.cyan}https://${data.fqdn}${colors.reset}`);
246
+ console.log(` ${colors.muted}URL:${colors.reset} ${colors.accent}https://${data.fqdn}${colors.reset}`);
172
247
  } else {
173
- print(` URL: ${colors.dim}(provisioning...)${colors.reset}`);
248
+ console.log(` ${colors.muted}URL:${colors.reset} ${colors.muted}(provisioning...)${colors.reset}`);
174
249
  }
175
250
 
176
251
  // Last provisioned
177
- print(` Last Deployed: ${formatDate(data.lastProvisioned)}`);
252
+ console.log(` ${colors.muted}Deployed:${colors.reset} ${formatDate(data.lastProvisioned)}`);
178
253
 
179
254
  // Error (if any)
180
255
  if (data.lastError) {
181
- print(` ${colors.red}Error: ${data.lastError}${colors.reset}`);
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(`${colors.bold}Connect to your Moltbot:${colors.reset}`);
189
- print(` Dashboard: ${colors.cyan}https://${data.fqdn}${colors.reset}`);
190
- print(` Gateway: ${colors.cyan}https://${data.fqdn}:18789${colors.reset}`);
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
- print(`\n${colors.bold}Recent Deployments${colors.reset}`);
202
- print("─".repeat(60));
277
+ printHeader("Recent Deployments");
203
278
 
204
279
  if (!data.deployments || data.deployments.length === 0) {
205
- print(" No deployments found");
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
- print(` ${colors.dim}[${date}]${colors.reset} ${dep.kind} - ${status}`);
288
+ console.log(` ${colors.muted}[${date}]${colors.reset} ${dep.kind} - ${status}`);
214
289
  if (dep.error) {
215
- print(` ${colors.red}Error: ${dep.error.substring(0, 60)}...${colors.reset}`);
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.red);
249
- print(`Run 'moltengine help' for usage information`);
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
+ };