haltest 1.0.0
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/bin/haltest.js +195 -0
- package/package.json +34 -0
- package/src/index.js +218 -0
package/bin/haltest.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* HalTest CLI Launcher — bin/haltest.js
|
|
5
|
+
*
|
|
6
|
+
* Starts the HalTest backend server, runs pre-requisite checks,
|
|
7
|
+
* auto-opens the browser, and handles graceful shutdown.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawn } from 'child_process';
|
|
11
|
+
import net from 'net';
|
|
12
|
+
import path from 'path';
|
|
13
|
+
import { fileURLToPath } from 'url';
|
|
14
|
+
|
|
15
|
+
// ── Resolve paths ──────────────────────────────────────────────────────────
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
// bin/ is one level inside apps/cli → go up two levels to reach the monorepo root
|
|
18
|
+
const MONOREPO_ROOT = path.resolve(__dirname, '..', '..', '..');
|
|
19
|
+
const BACKEND_ENTRY = path.join(MONOREPO_ROOT, 'apps', 'backend', 'app.js');
|
|
20
|
+
|
|
21
|
+
const PORT = parseInt(process.env.PORT || '2001', 10);
|
|
22
|
+
const APP_URL = `http://localhost:${PORT}/app/`;
|
|
23
|
+
const OLLAMA_PORT = 11434;
|
|
24
|
+
|
|
25
|
+
// ── ANSI helpers ───────────────────────────────────────────────────────────
|
|
26
|
+
const c = {
|
|
27
|
+
reset: '\x1b[0m',
|
|
28
|
+
bold: '\x1b[1m',
|
|
29
|
+
dim: '\x1b[2m',
|
|
30
|
+
cyan: '\x1b[36m',
|
|
31
|
+
green: '\x1b[32m',
|
|
32
|
+
yellow: '\x1b[33m',
|
|
33
|
+
red: '\x1b[31m',
|
|
34
|
+
magenta: '\x1b[35m',
|
|
35
|
+
blue: '\x1b[34m',
|
|
36
|
+
white: '\x1b[97m',
|
|
37
|
+
};
|
|
38
|
+
const style = (color, text) => `${color}${text}${c.reset}`;
|
|
39
|
+
|
|
40
|
+
// ── Banner ─────────────────────────────────────────────────────────────────
|
|
41
|
+
function printBanner() {
|
|
42
|
+
console.log('');
|
|
43
|
+
console.log(style(c.cyan + c.bold, ' ██╗ ██╗ █████╗ ██╗ ████████╗███████╗███████╗████████╗'));
|
|
44
|
+
console.log(style(c.cyan + c.bold, ' ██║ ██║██╔══██╗██║ ╚══██╔══╝██╔════╝██╔════╝╚══██╔══╝'));
|
|
45
|
+
console.log(style(c.cyan + c.bold, ' ███████║███████║██║ ██║ █████╗ ███████╗ ██║ '));
|
|
46
|
+
console.log(style(c.cyan + c.bold, ' ██╔══██║██╔══██║██║ ██║ ██╔══╝ ╚════██║ ██║ '));
|
|
47
|
+
console.log(style(c.cyan + c.bold, ' ██║ ██║██║ ██║███████╗██║ ███████╗███████║ ██║ '));
|
|
48
|
+
console.log(style(c.cyan + c.bold, ' ╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝╚═╝ ╚══════╝╚══════╝ ╚═╝ '));
|
|
49
|
+
console.log('');
|
|
50
|
+
console.log(style(c.white + c.bold, ' Browser Automation & Orchestration Platform'));
|
|
51
|
+
console.log(style(c.dim + c.white, ' ─────────────────────────────────────────────────────────'));
|
|
52
|
+
console.log('');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Port check helper ──────────────────────────────────────────────────────
|
|
56
|
+
function isPortListening(port) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
const socket = net.createConnection({ port, host: '127.0.0.1' });
|
|
59
|
+
socket.once('connect', () => { socket.destroy(); resolve(true); });
|
|
60
|
+
socket.once('error', () => { socket.destroy(); resolve(false); });
|
|
61
|
+
socket.setTimeout(500, () => { socket.destroy(); resolve(false); });
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── Pre-requisite checks ───────────────────────────────────────────────────
|
|
66
|
+
async function runPreRequisiteChecks() {
|
|
67
|
+
console.log(style(c.bold, ' 🔍 Running pre-requisite checks...\n'));
|
|
68
|
+
|
|
69
|
+
// Check Ollama
|
|
70
|
+
const ollamaOk = await isPortListening(OLLAMA_PORT);
|
|
71
|
+
if (ollamaOk) {
|
|
72
|
+
console.log(style(c.green, ` ✅ Ollama — detected on port ${OLLAMA_PORT}. Local AI is active.`));
|
|
73
|
+
} else {
|
|
74
|
+
console.log(
|
|
75
|
+
style(c.yellow, ` ⚠️ Ollama — not detected on port ${OLLAMA_PORT}.`) +
|
|
76
|
+
style(c.dim, ' Local AI features will be disabled until Ollama is started.')
|
|
77
|
+
);
|
|
78
|
+
console.log(
|
|
79
|
+
style(c.dim, ` Run: `) +
|
|
80
|
+
style(c.white, 'ollama serve') +
|
|
81
|
+
style(c.dim, ' in a separate terminal to enable AI features.')
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check Playwright (probe for the chromium executable)
|
|
86
|
+
try {
|
|
87
|
+
const { chromium } = await import('playwright');
|
|
88
|
+
const execPath = chromium.executablePath();
|
|
89
|
+
// executablePath() throws if not installed
|
|
90
|
+
void execPath;
|
|
91
|
+
console.log(style(c.green, ' ✅ Playwright — Chromium browser found.'));
|
|
92
|
+
} catch {
|
|
93
|
+
console.log(
|
|
94
|
+
style(c.yellow, ' ⚠️ Playwright — Chromium not found.') +
|
|
95
|
+
style(c.dim, ' Run: npx playwright install chromium')
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log('');
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Open browser (dynamic import for ESM compatibility) ────────────────────
|
|
103
|
+
async function openBrowser(url) {
|
|
104
|
+
try {
|
|
105
|
+
const { default: open } = await import('open');
|
|
106
|
+
await open(url);
|
|
107
|
+
} catch {
|
|
108
|
+
console.log(style(c.dim, ` ℹ️ Could not auto-open browser. Navigate to: ${url}`));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ── Main ───────────────────────────────────────────────────────────────────
|
|
113
|
+
printBanner();
|
|
114
|
+
|
|
115
|
+
await runPreRequisiteChecks();
|
|
116
|
+
|
|
117
|
+
console.log(style(c.bold, ` 🚀 Starting HalTest Server...`));
|
|
118
|
+
console.log(style(c.dim, ` Entry: ${BACKEND_ENTRY}`));
|
|
119
|
+
console.log('');
|
|
120
|
+
|
|
121
|
+
// Spawn the backend server
|
|
122
|
+
const server = spawn('node', [BACKEND_ENTRY], {
|
|
123
|
+
cwd: MONOREPO_ROOT,
|
|
124
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
125
|
+
env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'production' },
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
let browserOpened = false;
|
|
129
|
+
|
|
130
|
+
// Forward stdout — detect ready signal
|
|
131
|
+
server.stdout.on('data', (data) => {
|
|
132
|
+
const text = data.toString();
|
|
133
|
+
process.stdout.write(text);
|
|
134
|
+
|
|
135
|
+
// Detect the backend "ready" line
|
|
136
|
+
if (!browserOpened && text.includes('HaltTest Server is Up')) {
|
|
137
|
+
browserOpened = true;
|
|
138
|
+
console.log('');
|
|
139
|
+
console.log(style(c.green + c.bold, ` ✅ Server is up! Opening browser at ${APP_URL}`));
|
|
140
|
+
console.log(style(c.dim, ' Press Ctrl+C to stop the server.\n'));
|
|
141
|
+
openBrowser(APP_URL).catch(() => { });
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
// Forward stderr
|
|
146
|
+
server.stderr.on('data', (data) => {
|
|
147
|
+
process.stderr.write(data);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Handle unexpected server exit
|
|
151
|
+
server.on('exit', (code, signal) => {
|
|
152
|
+
if (signal) {
|
|
153
|
+
// Killed by us during shutdown — silent
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
if (code !== 0) {
|
|
157
|
+
console.error(style(c.red, `\n ❌ Server exited with code ${code}.`));
|
|
158
|
+
process.exit(code ?? 1);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
server.on('error', (err) => {
|
|
163
|
+
console.error(style(c.red, `\n ❌ Failed to start server: ${err.message}`));
|
|
164
|
+
console.error(style(c.dim, ` Make sure you're inside the HalTest monorepo directory.`));
|
|
165
|
+
process.exit(1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
// ── Graceful Shutdown ──────────────────────────────────────────────────────
|
|
169
|
+
function shutdown(signal) {
|
|
170
|
+
console.log('');
|
|
171
|
+
console.log(style(c.magenta + c.bold, `\n 👋 Received ${signal}. Shutting down HalTest gracefully...`));
|
|
172
|
+
|
|
173
|
+
if (!server.killed) {
|
|
174
|
+
server.kill('SIGTERM');
|
|
175
|
+
|
|
176
|
+
// Force-kill after 5 s if the child doesn't exit
|
|
177
|
+
const forceKill = setTimeout(() => {
|
|
178
|
+
if (!server.killed) {
|
|
179
|
+
server.kill('SIGKILL');
|
|
180
|
+
}
|
|
181
|
+
}, 5000);
|
|
182
|
+
|
|
183
|
+
server.on('exit', () => {
|
|
184
|
+
clearTimeout(forceKill);
|
|
185
|
+
console.log(style(c.green, ' ✅ Server stopped cleanly. Goodbye!\n'));
|
|
186
|
+
process.exit(0);
|
|
187
|
+
});
|
|
188
|
+
} else {
|
|
189
|
+
console.log(style(c.green, ' ✅ Server already stopped. Goodbye!\n'));
|
|
190
|
+
process.exit(0);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
195
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "haltest",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "CLI launcher for the HalTest browser automation & orchestration platform",
|
|
5
|
+
"main": "bin/haltest.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"haltest": "./bin/haltest.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"src"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"start": "node bin/haltest.js",
|
|
16
|
+
"dev": "node bin/haltest.js",
|
|
17
|
+
"cli": "node src/index.js"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"axios": "^1.7.9",
|
|
21
|
+
"chalk": "^5.4.1",
|
|
22
|
+
"commander": "^13.1.0",
|
|
23
|
+
"dotenv": "^16.4.7",
|
|
24
|
+
"form-data": "^4.0.1",
|
|
25
|
+
"open": "^10.1.0",
|
|
26
|
+
"ora": "^8.2.0",
|
|
27
|
+
"socket.io-client": "^4.8.3",
|
|
28
|
+
"terminal-image": "^4.2.0"
|
|
29
|
+
},
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=20.0.0"
|
|
32
|
+
},
|
|
33
|
+
"private": false
|
|
34
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import axios from "axios";
|
|
7
|
+
import dotenv from "dotenv";
|
|
8
|
+
import { fileURLToPath } from "url";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { io } from "socket.io-client";
|
|
11
|
+
import fs from "fs/promises";
|
|
12
|
+
|
|
13
|
+
// Load .env
|
|
14
|
+
dotenv.config();
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
// Use baseUrl but fallback to localhost:2001
|
|
18
|
+
const API_URL = (
|
|
19
|
+
process.env.HALTEST_API_URL || "http://localhost:2001/api"
|
|
20
|
+
).replace(/\/$/, "");
|
|
21
|
+
const SOCKET_URL = API_URL.replace("/api", "");
|
|
22
|
+
|
|
23
|
+
const program = new Command();
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.name("haltest")
|
|
27
|
+
.description("CLI for HAL-TEST automation framework")
|
|
28
|
+
.version("1.0.0");
|
|
29
|
+
|
|
30
|
+
// --- STATUS COMMAND ---
|
|
31
|
+
program
|
|
32
|
+
.command("status")
|
|
33
|
+
.description("Check the status of the HAL-TEST server")
|
|
34
|
+
.action(async () => {
|
|
35
|
+
const spinner = ora("Connecting to HAL-TEST server...").start();
|
|
36
|
+
try {
|
|
37
|
+
const response = await axios.get(`${API_URL}/status`);
|
|
38
|
+
spinner.succeed(chalk.green("HAL-TEST Server is ONLINE"));
|
|
39
|
+
console.log(chalk.gray(`URL: ${API_URL}`));
|
|
40
|
+
console.log(
|
|
41
|
+
chalk.cyan(`Environment: ${response.data.environment || "N/A"}`),
|
|
42
|
+
);
|
|
43
|
+
} catch (error) {
|
|
44
|
+
spinner.fail(chalk.red("HAL-TEST Server is OFFLINE"));
|
|
45
|
+
console.error(chalk.gray(`Tried: ${API_URL}`));
|
|
46
|
+
console.error(chalk.red(error.message));
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// --- LIST COMMAND ---
|
|
51
|
+
program
|
|
52
|
+
.command("list")
|
|
53
|
+
.description("List all available flows")
|
|
54
|
+
.action(async () => {
|
|
55
|
+
const spinner = ora("Fetching flows...").start();
|
|
56
|
+
try {
|
|
57
|
+
const projectsRes = await axios.get(`${API_URL}/projects`);
|
|
58
|
+
spinner.succeed(chalk.blue("Available Flows:"));
|
|
59
|
+
|
|
60
|
+
projectsRes.data.forEach((project) => {
|
|
61
|
+
console.log(
|
|
62
|
+
`\n📂 Project: ${chalk.bold(project.name)} ${chalk.gray(`(${project.id})`)}`,
|
|
63
|
+
);
|
|
64
|
+
project.flows?.forEach((flow) => {
|
|
65
|
+
console.log(
|
|
66
|
+
` - ${chalk.cyan(flow.name)} ${chalk.gray(`ID: ${flow.id}`)}`,
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
spinner.fail(chalk.red("Failed to fetch flows"));
|
|
72
|
+
console.error(chalk.red(error.message));
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// --- RUN COMMAND ---
|
|
77
|
+
program
|
|
78
|
+
.command("run <flowId>")
|
|
79
|
+
.description("Execute a flow and stream real-time logs")
|
|
80
|
+
.option("-p, --project <projectId>", "Project ID", "default-project-1")
|
|
81
|
+
.option(
|
|
82
|
+
"-h, --headed",
|
|
83
|
+
"Run with visible browser (overrides headless)",
|
|
84
|
+
false,
|
|
85
|
+
)
|
|
86
|
+
.option("-o, --output <path>", "Save execution report to file (JSON)")
|
|
87
|
+
.action(async (flowId, options) => {
|
|
88
|
+
const { project: projectId, headed, output } = options;
|
|
89
|
+
|
|
90
|
+
console.log(
|
|
91
|
+
chalk.bold.blue(`\n🚀 Initializing execution for flow: ${flowId}`),
|
|
92
|
+
);
|
|
93
|
+
const spinner = ora("Handshaking with server...").start();
|
|
94
|
+
|
|
95
|
+
// 1. Connect to Socket for logs
|
|
96
|
+
const socket = io(SOCKET_URL);
|
|
97
|
+
let currentRunId = null;
|
|
98
|
+
|
|
99
|
+
socket.on("connect", () => {
|
|
100
|
+
spinner.text = "Connected to socket, waiting for execution start...";
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
socket.on("execution-log", (data) => {
|
|
104
|
+
const time = chalk.gray(
|
|
105
|
+
`[${new Date(data.timestamp).toLocaleTimeString()}]`,
|
|
106
|
+
);
|
|
107
|
+
const node = data.nodeId
|
|
108
|
+
? chalk.magenta(`[${data.nodeId.split("-")[0]}]`)
|
|
109
|
+
: "";
|
|
110
|
+
|
|
111
|
+
let msg = data.message;
|
|
112
|
+
if (data.type === "error") msg = chalk.red(msg);
|
|
113
|
+
else if (data.type === "success") msg = chalk.green(msg);
|
|
114
|
+
else if (data.type === "warning") msg = chalk.yellow(msg);
|
|
115
|
+
|
|
116
|
+
console.log(`${time} ${node} ${msg}`);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
socket.on("execution-status", (data) => {
|
|
120
|
+
if (data.status === "failed") {
|
|
121
|
+
console.log(
|
|
122
|
+
chalk.red(
|
|
123
|
+
`\n❌ Node [${data.stepId}] failed: ${data.error || "Unknown error"}`,
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// AUTO-TERMINATE LOGIC
|
|
130
|
+
socket.on("flow-finished", async (data) => {
|
|
131
|
+
if (data.runId !== currentRunId) return;
|
|
132
|
+
|
|
133
|
+
console.log(chalk.bold("\n" + "=".repeat(40)));
|
|
134
|
+
console.log(
|
|
135
|
+
chalk.bold(`🏁 EXECUTION FINISHED: ${data.status.toUpperCase()}`),
|
|
136
|
+
);
|
|
137
|
+
console.log(chalk.bold("=".repeat(40) + "\n"));
|
|
138
|
+
|
|
139
|
+
const reportSpinner = ora("Generating final report...").start();
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// Fetch full report from API
|
|
143
|
+
const runRes = await axios.get(`${API_URL}/runs/${currentRunId}`);
|
|
144
|
+
const runData = runRes.data.data;
|
|
145
|
+
reportSpinner.succeed("Report generated");
|
|
146
|
+
|
|
147
|
+
// Print summary table-like info
|
|
148
|
+
console.log(chalk.cyan(`Run ID: `) + runData.id);
|
|
149
|
+
console.log(
|
|
150
|
+
chalk.cyan(`Duration: `) +
|
|
151
|
+
`${(runData.duration_ms / 1000).toFixed(2)}s`,
|
|
152
|
+
);
|
|
153
|
+
console.log(chalk.cyan(`Steps: `) + runData.steps?.length);
|
|
154
|
+
|
|
155
|
+
const errors =
|
|
156
|
+
runData.steps?.filter((s) => s.status === "failed") || [];
|
|
157
|
+
if (errors.length > 0) {
|
|
158
|
+
console.log(chalk.red(`Errors: `) + errors.length);
|
|
159
|
+
errors.forEach((e) => {
|
|
160
|
+
console.log(
|
|
161
|
+
chalk.red(` ! [${e.node_id}] ${e.node_type}: ${e.error}`),
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
} else {
|
|
165
|
+
console.log(chalk.green(`Errors: 0`));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Save to file if requested
|
|
169
|
+
if (output) {
|
|
170
|
+
await fs.writeFile(output, JSON.stringify(runData, null, 2));
|
|
171
|
+
console.log(chalk.green(`\n💾 Report saved to: ${output}`));
|
|
172
|
+
}
|
|
173
|
+
} catch (err) {
|
|
174
|
+
reportSpinner.fail("Failed to fetch run details");
|
|
175
|
+
console.error(chalk.red(err.message));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
socket.disconnect();
|
|
179
|
+
process.exit(data.status === "completed" ? 0 : 1);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
// 2. Trigger the run
|
|
184
|
+
const response = await axios.post(`${API_URL}/runs/start`, {
|
|
185
|
+
flowId,
|
|
186
|
+
projectId,
|
|
187
|
+
overrides: {
|
|
188
|
+
headless: !headed,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
currentRunId = response.data.runId;
|
|
193
|
+
spinner.succeed(
|
|
194
|
+
chalk.green(`Execution started! Run ID: ${currentRunId}`),
|
|
195
|
+
);
|
|
196
|
+
console.log(chalk.gray("--- Streaming Logs ---\n"));
|
|
197
|
+
} catch (error) {
|
|
198
|
+
spinner.fail(chalk.red("Execution failed to start"));
|
|
199
|
+
socket.disconnect();
|
|
200
|
+
if (error.response) {
|
|
201
|
+
const backendError =
|
|
202
|
+
error.response.data.message ||
|
|
203
|
+
error.response.data.error ||
|
|
204
|
+
error.response.statusText;
|
|
205
|
+
console.error(chalk.red(`Error: ${backendError}`));
|
|
206
|
+
} else {
|
|
207
|
+
console.error(chalk.red(error.message));
|
|
208
|
+
}
|
|
209
|
+
process.exit(1);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// Show help if no command is provided
|
|
214
|
+
if (!process.argv.slice(2).length) {
|
|
215
|
+
program.outputHelp();
|
|
216
|
+
} else {
|
|
217
|
+
program.parse();
|
|
218
|
+
}
|