hackerrun 0.1.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.
@@ -0,0 +1,227 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
5
+ import { ClusterManager } from '../lib/cluster.js';
6
+ import { PlatformDetector } from '../lib/platform.js';
7
+ import { UncloudManager } from '../lib/uncloud.js';
8
+ import { getPlatformToken } from '../lib/platform-auth.js';
9
+ import { PlatformClient } from '../lib/platform-client.js';
10
+ import { getAppName, hasAppConfig, linkApp } from '../lib/app-config.js';
11
+ import { UncloudRunner } from '../lib/uncloud-runner.js';
12
+
13
+ // Default VM settings
14
+ const DEFAULT_LOCATION = 'eu-central-h1';
15
+ const DEFAULT_SIZE = 'burstable-1';
16
+ const DEFAULT_STORAGE_SIZE = 10; // GB
17
+ const DEFAULT_IMAGE = 'ubuntu-noble';
18
+
19
+ /**
20
+ * Inject domain into compose file using x-ports extension
21
+ * This allows custom domain routing without modifying uncloud-dns
22
+ */
23
+ function injectDomainIntoCompose(composePath: string, domain: string): void {
24
+ if (!existsSync(composePath)) {
25
+ return;
26
+ }
27
+
28
+ const content = readFileSync(composePath, 'utf-8');
29
+
30
+ // Check if x-ports already exists with our domain
31
+ if (content.includes(`x-ports:`)) {
32
+ // Domain injection already present, skip
33
+ return;
34
+ }
35
+
36
+ // Find services that have ports defined and inject x-ports
37
+ // This is a simple approach - we add x-ports to services with ports
38
+ const lines = content.split('\n');
39
+ const newLines: string[] = [];
40
+ let inService = false;
41
+ let currentIndent = 0;
42
+ let hasInjectedXPorts = false;
43
+
44
+ for (let i = 0; i < lines.length; i++) {
45
+ const line = lines[i];
46
+ newLines.push(line);
47
+
48
+ // Check if we're entering a service that has ports
49
+ if (line.match(/^\s+ports:/) && !hasInjectedXPorts) {
50
+ // Add x-ports after the ports section ends
51
+ // Find the indent level
52
+ const indent = line.match(/^(\s*)/)?.[1] || ' ';
53
+
54
+ // Look ahead for the port value(s)
55
+ let j = i + 1;
56
+ while (j < lines.length && lines[j].match(/^\s+-/)) {
57
+ newLines.push(lines[j]);
58
+ j++;
59
+ }
60
+ i = j - 1;
61
+
62
+ // Inject x-ports with our domain
63
+ newLines.push(`${indent}x-ports:`);
64
+ newLines.push(`${indent} - "443:443"`);
65
+ newLines.push(`${indent} host: "${domain}"`);
66
+ hasInjectedXPorts = true;
67
+ }
68
+ }
69
+
70
+ // Only write if we made changes
71
+ if (hasInjectedXPorts) {
72
+ writeFileSync(composePath, newLines.join('\n'), 'utf-8');
73
+ }
74
+ }
75
+
76
+ export function createDeployCommand() {
77
+ const cmd = new Command('deploy');
78
+ cmd
79
+ .description('Deploy your application to hackerrun')
80
+ .option('-n, --name <name>', 'App name (defaults to hackerrun.yaml or directory name)')
81
+ .option('--app <app>', 'App name (alias for --name, for CI/CD compatibility)')
82
+ .option('-l, --location <location>', 'VM location')
83
+ .option('-s, --size <size>', 'VM size (default: burstable-1)')
84
+ .option('--storage <gb>', 'Storage size in GB (default: 10)')
85
+ .option('-i, --image <image>', 'Boot image')
86
+ .option('--build-token <token>', 'Build token for CI/CD (bypasses normal auth)')
87
+ .action(async (options) => {
88
+ try {
89
+ // Check platform compatibility
90
+ PlatformDetector.ensureSupported();
91
+
92
+ // Check uncloud CLI is installed (offers to auto-install if missing)
93
+ await UncloudManager.ensureInstalled();
94
+
95
+ // Determine authentication mode
96
+ // CI/CD mode: use build token if provided
97
+ // Interactive mode: use user's platform token
98
+ let platformToken: string;
99
+ const isCIBuild = !!options.buildToken;
100
+
101
+ if (isCIBuild) {
102
+ platformToken = options.buildToken;
103
+ } else {
104
+ platformToken = getPlatformToken();
105
+ }
106
+
107
+ const platformClient = new PlatformClient(platformToken);
108
+ const clusterManager = new ClusterManager(platformClient);
109
+ const uncloudRunner = new UncloudRunner(platformClient);
110
+
111
+ // Determine app name: --app or --name flag > hackerrun.yaml > directory name
112
+ const appName = options.app || options.name || getAppName();
113
+
114
+ // Resolve VM settings
115
+ const location = options.location || DEFAULT_LOCATION;
116
+ const vmSize = options.size || DEFAULT_SIZE;
117
+ const storageSize = options.storage ? parseInt(options.storage) : DEFAULT_STORAGE_SIZE;
118
+ const bootImage = options.image || DEFAULT_IMAGE;
119
+
120
+ console.log(chalk.cyan(`\nDeploying '${appName}' to hackerrun...\n`));
121
+
122
+ // Check if app already has infrastructure (from backend)
123
+ let cluster = await platformClient.getApp(appName);
124
+ let isFirstDeploy = false;
125
+
126
+ if (!cluster) {
127
+ // First deployment - create infrastructure
128
+ isFirstDeploy = true;
129
+ console.log(chalk.yellow('First deployment - creating infrastructure...\n'));
130
+
131
+ // Initialize cluster (saves to platform and returns with domainName)
132
+ cluster = await clusterManager.initializeCluster({
133
+ appName,
134
+ location,
135
+ vmSize,
136
+ storageSize,
137
+ bootImage,
138
+ });
139
+
140
+ console.log(chalk.cyan('\nWhat just happened:'));
141
+ console.log(` ${chalk.green('✓')} Created 1 IPv6-only VM (${vmSize})`);
142
+ console.log(` ${chalk.green('✓')} Installed Docker and Uncloud daemon`);
143
+ console.log(` ${chalk.green('✓')} Initialized uncloud cluster`);
144
+ console.log(` ${chalk.green('✓')} Assigned domain: ${cluster.domainName}.hackerrun.app`);
145
+ console.log();
146
+
147
+ // Create hackerrun.yaml if not already present
148
+ if (!hasAppConfig()) {
149
+ linkApp(appName);
150
+ console.log(chalk.dim(`Created hackerrun.yaml for app linking\n`));
151
+ }
152
+ } else {
153
+ console.log(chalk.green(`Using existing infrastructure (${cluster.nodes.length} VM(s))\n`));
154
+ }
155
+
156
+ // Get primary node from backend
157
+ const primaryNode = await platformClient.getPrimaryNode(appName);
158
+ if (!primaryNode || !primaryNode.ipv6) {
159
+ throw new Error('Primary node not found or has no IPv6 address');
160
+ }
161
+
162
+ // Deploy using uncloud with SSH certificate authentication
163
+ console.log(chalk.cyan('\nRunning deployment...\n'));
164
+
165
+ try {
166
+ // Run uc deploy using ssh+cli:// connector
167
+ await uncloudRunner.deploy(appName, process.cwd());
168
+
169
+ console.log(chalk.green('\nApp deployed successfully!'));
170
+
171
+ // Register route for this app (maps domain to VM IPv6, port 80 for HTTP)
172
+ if (cluster.domainName) {
173
+ const spinner = ora('Registering route...').start();
174
+ try {
175
+ const route = await platformClient.registerRoute(appName, primaryNode.ipv6, 80);
176
+ spinner.succeed(chalk.green(`Route registered: ${route.fullUrl}`));
177
+
178
+ // Sync gateway Caddy config with all routes
179
+ spinner.start('Syncing gateway routes...');
180
+ await platformClient.syncGatewayRoutes(cluster.location);
181
+ spinner.succeed(chalk.green('Gateway routes synced'));
182
+ } catch (error) {
183
+ spinner.warn(chalk.yellow(`Could not register route: ${(error as Error).message}`));
184
+ }
185
+ }
186
+
187
+ // Update last deployed timestamp on backend
188
+ await platformClient.updateLastDeployed(appName);
189
+
190
+ // Show summary
191
+ console.log(chalk.cyan('\nDeployment Summary:\n'));
192
+ console.log(` App Name: ${chalk.bold(appName)}`);
193
+ console.log(` Domain: ${chalk.bold(`${cluster.domainName}.hackerrun.app`)}`);
194
+ console.log(` URL: ${chalk.bold(`https://${cluster.domainName}.hackerrun.app`)}`);
195
+ console.log(` Location: ${cluster.location}`);
196
+ console.log(` Nodes: ${cluster.nodes.length}`);
197
+
198
+ console.log(chalk.cyan('\nInfrastructure:\n'));
199
+ cluster.nodes.forEach((node, index) => {
200
+ const prefix = node.isPrimary ? chalk.yellow('(primary)') : ' ';
201
+ const ip = node.ipv6 || node.ipv4 || 'pending';
202
+ console.log(` ${prefix} ${node.name} - ${ip}`);
203
+ });
204
+
205
+ console.log(chalk.cyan('\nNext steps:\n'));
206
+ console.log(` View logs: ${chalk.bold(`hackerrun logs ${appName}`)}`);
207
+ console.log(` SSH access: ${chalk.bold(`hackerrun ssh ${appName}`)}`);
208
+ console.log(` Change domain: ${chalk.bold(`hackerrun domain --app ${appName} <new-name>`)}`);
209
+ console.log();
210
+
211
+ // Cleanup SSH sessions and tunnels
212
+ uncloudRunner.cleanup();
213
+
214
+ } catch (error) {
215
+ uncloudRunner.cleanup();
216
+ console.log(chalk.red('\nDeployment failed'));
217
+ throw error;
218
+ }
219
+
220
+ } catch (error) {
221
+ console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
222
+ process.exit(1);
223
+ }
224
+ });
225
+
226
+ return cmd;
227
+ }
@@ -0,0 +1,174 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { password } from '@inquirer/prompts';
5
+ import { readFileSync, existsSync } from 'fs';
6
+ import { getPlatformToken } from '../lib/platform-auth.js';
7
+ import { PlatformClient } from '../lib/platform-client.js';
8
+ import { getAppName } from '../lib/app-config.js';
9
+
10
+ /**
11
+ * Parse a .env file into key-value pairs
12
+ */
13
+ function parseEnvFile(content: string): Record<string, string> {
14
+ const vars: Record<string, string> = {};
15
+ for (const line of content.split('\n')) {
16
+ const trimmed = line.trim();
17
+ if (!trimmed || trimmed.startsWith('#')) continue;
18
+
19
+ const eqIndex = trimmed.indexOf('=');
20
+ if (eqIndex === -1) continue;
21
+
22
+ const key = trimmed.slice(0, eqIndex);
23
+ let value = trimmed.slice(eqIndex + 1);
24
+
25
+ // Handle quoted values
26
+ if ((value.startsWith('"') && value.endsWith('"')) ||
27
+ (value.startsWith("'") && value.endsWith("'"))) {
28
+ value = value.slice(1, -1);
29
+ }
30
+
31
+ vars[key] = value;
32
+ }
33
+ return vars;
34
+ }
35
+
36
+ export function createEnvCommand() {
37
+ const cmd = new Command('env');
38
+ cmd.description('Manage environment variables for an app');
39
+
40
+ // env set KEY=value or env set KEY (prompts for value)
41
+ cmd
42
+ .command('set')
43
+ .description('Set an environment variable')
44
+ .argument('<keyValue>', 'KEY=value or just KEY (will prompt for value)')
45
+ .option('--app <app>', 'App name (uses hackerrun.yaml if not specified)')
46
+ .action(async (keyValue: string, options) => {
47
+ try {
48
+ const appName = options.app || getAppName();
49
+ const platformToken = getPlatformToken();
50
+ const platformClient = new PlatformClient(platformToken);
51
+
52
+ let key: string;
53
+ let value: string;
54
+
55
+ if (keyValue.includes('=')) {
56
+ const parts = keyValue.split('=');
57
+ key = parts[0];
58
+ value = parts.slice(1).join('='); // Handle values with = in them
59
+ } else {
60
+ key = keyValue;
61
+ // Prompt for hidden input
62
+ value = await password({
63
+ message: `Enter value for ${key}:`,
64
+ mask: '*',
65
+ });
66
+ }
67
+
68
+ if (!key) {
69
+ console.error(chalk.red('\nError: Key cannot be empty\n'));
70
+ process.exit(1);
71
+ }
72
+
73
+ const spinner = ora(`Setting ${key}...`).start();
74
+ await platformClient.setEnvVar(appName, key, value);
75
+ spinner.succeed(`Set ${key}`);
76
+ } catch (error) {
77
+ console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
78
+ process.exit(1);
79
+ }
80
+ });
81
+
82
+ // env upload .env
83
+ cmd
84
+ .command('upload')
85
+ .description('Upload environment variables from a file')
86
+ .argument('<file>', 'Path to .env file')
87
+ .option('--app <app>', 'App name (uses hackerrun.yaml if not specified)')
88
+ .action(async (filePath: string, options) => {
89
+ try {
90
+ const appName = options.app || getAppName();
91
+ const platformToken = getPlatformToken();
92
+ const platformClient = new PlatformClient(platformToken);
93
+
94
+ if (!existsSync(filePath)) {
95
+ console.error(chalk.red(`\nError: File not found: ${filePath}\n`));
96
+ process.exit(1);
97
+ }
98
+
99
+ const content = readFileSync(filePath, 'utf-8');
100
+ const vars = parseEnvFile(content);
101
+ const count = Object.keys(vars).length;
102
+
103
+ if (count === 0) {
104
+ console.log(chalk.yellow('\nNo environment variables found in file.\n'));
105
+ return;
106
+ }
107
+
108
+ const spinner = ora(`Uploading ${count} variable${count > 1 ? 's' : ''}...`).start();
109
+ await platformClient.setEnvVars(appName, vars);
110
+ spinner.succeed(`Uploaded ${count} environment variable${count > 1 ? 's' : ''}`);
111
+ } catch (error) {
112
+ console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
113
+ process.exit(1);
114
+ }
115
+ });
116
+
117
+ // env list
118
+ cmd
119
+ .command('list')
120
+ .description('List environment variables')
121
+ .option('--app <app>', 'App name (uses hackerrun.yaml if not specified)')
122
+ .action(async (options) => {
123
+ try {
124
+ const appName = options.app || getAppName();
125
+ const platformToken = getPlatformToken();
126
+ const platformClient = new PlatformClient(platformToken);
127
+
128
+ const spinner = ora('Fetching environment variables...').start();
129
+ const vars = await platformClient.listEnvVars(appName);
130
+ spinner.stop();
131
+
132
+ if (vars.length === 0) {
133
+ console.log(chalk.yellow(`\nNo environment variables set for '${appName}'.\n`));
134
+ console.log(chalk.cyan('Set variables with:\n'));
135
+ console.log(` hackerrun env set KEY=value`);
136
+ console.log(` hackerrun env upload .env\n`);
137
+ return;
138
+ }
139
+
140
+ console.log(chalk.cyan(`\nEnvironment variables for '${appName}':\n`));
141
+ for (const v of vars) {
142
+ const masked = '*'.repeat(Math.min(v.valueLength, 20));
143
+ console.log(` ${chalk.bold(v.key)}=${chalk.dim(masked)}`);
144
+ }
145
+ console.log();
146
+ } catch (error) {
147
+ console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
148
+ process.exit(1);
149
+ }
150
+ });
151
+
152
+ // env unset KEY
153
+ cmd
154
+ .command('unset')
155
+ .description('Remove an environment variable')
156
+ .argument('<key>', 'Environment variable key')
157
+ .option('--app <app>', 'App name (uses hackerrun.yaml if not specified)')
158
+ .action(async (key: string, options) => {
159
+ try {
160
+ const appName = options.app || getAppName();
161
+ const platformToken = getPlatformToken();
162
+ const platformClient = new PlatformClient(platformToken);
163
+
164
+ const spinner = ora(`Removing ${key}...`).start();
165
+ await platformClient.unsetEnvVar(appName, key);
166
+ spinner.succeed(`Removed ${key}`);
167
+ } catch (error) {
168
+ console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
169
+ process.exit(1);
170
+ }
171
+ });
172
+
173
+ return cmd;
174
+ }
@@ -0,0 +1,120 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { ConfigManager } from '../lib/config.js';
5
+
6
+ const PLATFORM_API_URL = process.env.HACKERRUN_API_URL || 'http://localhost:3000';
7
+
8
+ interface DeviceFlowInitiation {
9
+ deviceCode: string;
10
+ userCode: string;
11
+ verificationUri: string;
12
+ expiresIn: number;
13
+ interval: number;
14
+ }
15
+
16
+ interface DeviceFlowResult {
17
+ status: 'pending' | 'authorized' | 'expired';
18
+ token?: string;
19
+ }
20
+
21
+ export function createLoginCommand() {
22
+ const cmd = new Command('login');
23
+ cmd.description('Login to HackerRun platform');
24
+
25
+ cmd.action(async () => {
26
+ try {
27
+ console.log(chalk.cyan('\n🔐 Logging in to HackerRun\n'));
28
+
29
+ // Step 1: Initiate device flow
30
+ const spinner = ora('Initiating login...').start();
31
+
32
+ const initResponse = await fetch(`${PLATFORM_API_URL}/api/auth/device`, {
33
+ method: 'POST',
34
+ });
35
+
36
+ if (!initResponse.ok) {
37
+ spinner.fail('Failed to initiate login');
38
+ const error = await initResponse.json().catch(() => ({ error: 'Unknown error' }));
39
+ console.error(chalk.red(`\nError: ${error.error || initResponse.statusText}\n`));
40
+ process.exit(1);
41
+ }
42
+
43
+ const deviceFlow: DeviceFlowInitiation = await initResponse.json();
44
+ spinner.succeed('Login initiated');
45
+
46
+ // Step 2: Show user code and URL
47
+ console.log(chalk.cyan('\n📋 Please complete the following steps:\n'));
48
+ console.log(` 1. Visit: ${chalk.bold.blue(deviceFlow.verificationUri)}`);
49
+ console.log(` 2. Enter code: ${chalk.bold.yellow(deviceFlow.userCode)}\n`);
50
+ console.log(chalk.dim(`Code expires in ${Math.floor(deviceFlow.expiresIn / 60)} minutes\n`));
51
+
52
+ // Step 3: Poll for authorization
53
+ const pollSpinner = ora('Waiting for authorization...').start();
54
+
55
+ const pollInterval = deviceFlow.interval * 1000; // Convert to milliseconds
56
+ const maxAttempts = Math.floor(deviceFlow.expiresIn / deviceFlow.interval);
57
+ let attempts = 0;
58
+
59
+ while (attempts < maxAttempts) {
60
+ await sleep(pollInterval);
61
+ attempts++;
62
+
63
+ try {
64
+ const pollResponse = await fetch(
65
+ `${PLATFORM_API_URL}/api/auth/device/poll?device_code=${deviceFlow.deviceCode}`
66
+ );
67
+
68
+ if (!pollResponse.ok) {
69
+ pollSpinner.fail('Failed to check authorization status');
70
+ console.error(chalk.red('\nPlease try again later\n'));
71
+ process.exit(1);
72
+ }
73
+
74
+ const result: DeviceFlowResult = await pollResponse.json();
75
+
76
+ if (result.status === 'authorized') {
77
+ pollSpinner.succeed(chalk.green('Authorization successful!'));
78
+
79
+ // Save platform token
80
+ const configManager = new ConfigManager();
81
+ configManager.set('apiToken', result.token!);
82
+
83
+ console.log(chalk.green('\n✓ Login successful!\n'));
84
+ console.log(chalk.cyan('You can now use HackerRun CLI:\n'));
85
+ console.log(` ${chalk.bold('hackerrun deploy')} Deploy your application`);
86
+ console.log(` ${chalk.bold('hackerrun app list')} List your apps`);
87
+ console.log(` ${chalk.bold('hackerrun config list')} View configuration\n`);
88
+
89
+ return;
90
+ } else if (result.status === 'expired') {
91
+ pollSpinner.fail('Authorization code expired');
92
+ console.error(chalk.red('\nPlease run `hackerrun login` again\n'));
93
+ process.exit(1);
94
+ }
95
+
96
+ // Status is 'pending', continue polling
97
+ } catch (error) {
98
+ pollSpinner.fail('Failed to check authorization');
99
+ console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
100
+ process.exit(1);
101
+ }
102
+ }
103
+
104
+ // Timeout
105
+ pollSpinner.fail('Authorization timed out');
106
+ console.error(chalk.red('\nPlease run `hackerrun login` again\n'));
107
+ process.exit(1);
108
+
109
+ } catch (error) {
110
+ console.error(chalk.red(`\n❌ Error: ${(error as Error).message}\n`));
111
+ process.exit(1);
112
+ }
113
+ });
114
+
115
+ return cmd;
116
+ }
117
+
118
+ function sleep(ms: number): Promise<void> {
119
+ return new Promise(resolve => setTimeout(resolve, ms));
120
+ }
@@ -0,0 +1,97 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import { getPlatformToken } from '../lib/platform-auth.js';
4
+ import { PlatformClient } from '../lib/platform-client.js';
5
+ import { UncloudRunner } from '../lib/uncloud-runner.js';
6
+
7
+ export function createLogsCommand() {
8
+ const cmd = new Command('logs');
9
+
10
+ cmd
11
+ .description('View logs for app services')
12
+ .argument('<app>', 'App name')
13
+ .argument('[service]', 'Service name (optional - lists services if omitted)')
14
+ .option('-f, --follow', 'Follow log output')
15
+ .option('--tail <lines>', 'Number of lines to show from the end of the logs')
16
+ .action(async (appName: string, serviceName: string | undefined, options) => {
17
+ const platformToken = getPlatformToken();
18
+ const platformClient = new PlatformClient(platformToken);
19
+ const uncloudRunner = new UncloudRunner(platformClient);
20
+
21
+ try {
22
+ const app = await platformClient.getApp(appName);
23
+
24
+ if (!app) {
25
+ console.log(chalk.red(`\n App '${appName}' not found\n`));
26
+ console.log(chalk.cyan('Available apps:'));
27
+ const apps = await platformClient.listApps();
28
+ if (apps.length === 0) {
29
+ console.log(chalk.yellow(' No apps deployed yet\n'));
30
+ } else {
31
+ apps.forEach(a => console.log(` - ${a.appName}`));
32
+ console.log();
33
+ }
34
+ process.exit(1);
35
+ }
36
+
37
+ // If no service specified, list available services
38
+ if (!serviceName) {
39
+ try {
40
+ const output = await uncloudRunner.serviceList(appName);
41
+
42
+ // Parse table output - skip header line and extract service names
43
+ const lines = output.trim().split('\n');
44
+ const services: string[] = [];
45
+
46
+ for (let i = 0; i < lines.length; i++) {
47
+ const line = lines[i].trim();
48
+ // Skip header and empty lines
49
+ if (!line || line.startsWith('NAME') || line.startsWith('Connecting') || line.startsWith('Connected')) {
50
+ continue;
51
+ }
52
+ // Extract first column (service name)
53
+ const svcName = line.split(/\s+/)[0];
54
+ if (svcName) {
55
+ services.push(svcName);
56
+ }
57
+ }
58
+
59
+ if (services.length === 0) {
60
+ console.log(chalk.yellow(`\n No services found in app '${appName}'\n`));
61
+ return;
62
+ }
63
+
64
+ console.log(chalk.cyan(`\n Available services in '${appName}':\n`));
65
+ services.forEach(s => console.log(` ${chalk.bold(s)}`));
66
+ console.log(chalk.dim(`\nUsage: hackerrun logs ${appName} <service>`));
67
+ console.log(chalk.dim(`Example: hackerrun logs ${appName} ${services[0]}\n`));
68
+ return;
69
+ } catch (error) {
70
+ console.error(chalk.red('\n Failed to list services'));
71
+ console.error(chalk.yellow('Make sure the app is deployed and running\n'));
72
+ process.exit(1);
73
+ }
74
+ }
75
+
76
+ console.log(chalk.cyan(`\n Viewing logs for '${serviceName}' in app '${appName}'...\n`));
77
+
78
+ try {
79
+ await uncloudRunner.logs(appName, serviceName, {
80
+ follow: options.follow,
81
+ tail: options.tail ? parseInt(options.tail) : undefined,
82
+ });
83
+ } catch (error) {
84
+ console.error(chalk.red(`\n Failed to fetch logs for service '${serviceName}'`));
85
+ console.error(chalk.yellow('The service may not exist or may not be running\n'));
86
+ process.exit(1);
87
+ }
88
+ } catch (error) {
89
+ console.error(chalk.red(`\n Error: ${(error as Error).message}\n`));
90
+ process.exit(1);
91
+ } finally {
92
+ uncloudRunner.cleanup();
93
+ }
94
+ });
95
+
96
+ return cmd;
97
+ }
package/src/index.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { Command } from 'commander';
2
+ import { createLoginCommand } from './commands/login.js';
3
+ import { createDeployCommand } from './commands/deploy.js';
4
+ import { createAppCommands } from './commands/app.js';
5
+ import { createConfigCommand } from './commands/config.js';
6
+ import { createLogsCommand } from './commands/logs.js';
7
+ import { createEnvCommand } from './commands/env.js';
8
+ import { createConnectCommand, createDisconnectCommand } from './commands/connect.js';
9
+ import { createBuildsCommand } from './commands/builds.js';
10
+
11
+ const program = new Command();
12
+
13
+ program
14
+ .name('hackerrun')
15
+ .description('Deploy apps with full control over your infrastructure')
16
+ .version('0.1.0');
17
+
18
+ // Register auth command (should be first)
19
+ program.addCommand(createLoginCommand());
20
+
21
+ // Register config command
22
+ program.addCommand(createConfigCommand());
23
+
24
+ // Register PaaS commands
25
+ program.addCommand(createDeployCommand());
26
+ program.addCommand(createLogsCommand());
27
+
28
+ // Register CI/CD commands
29
+ program.addCommand(createConnectCommand());
30
+ program.addCommand(createDisconnectCommand());
31
+ program.addCommand(createEnvCommand());
32
+ program.addCommand(createBuildsCommand());
33
+
34
+ const { appsCmd, nodesCmd, sshCmd, destroyCmd, linkCmd, renameCmd, domainCmd } = createAppCommands();
35
+ program.addCommand(appsCmd);
36
+ program.addCommand(nodesCmd);
37
+ program.addCommand(sshCmd);
38
+ program.addCommand(destroyCmd);
39
+ program.addCommand(linkCmd);
40
+ program.addCommand(renameCmd);
41
+ program.addCommand(domainCmd);
42
+
43
+ program.parse();