hackerrun 0.1.11 → 0.1.12

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/dist/index.js CHANGED
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
7
7
  });
8
8
 
9
9
  // src/index.ts
10
- import { Command as Command11 } from "commander";
10
+ import { Command as Command12 } from "commander";
11
11
 
12
12
  // src/commands/login.ts
13
13
  import { Command } from "commander";
@@ -1153,16 +1153,14 @@ var ClusterManager = class {
1153
1153
  */
1154
1154
  async initializeCluster(options) {
1155
1155
  const { appName, location, vmSize, storageSize, bootImage } = options;
1156
- const vmName = `${appName}-1`;
1157
- let spinner = ora2(`Creating VM '${vmName}' in ${location}...`).start();
1156
+ let spinner = ora2(`Creating VM for '${appName}' in ${location}...`).start();
1158
1157
  try {
1159
1158
  spinner.text = "Fetching platform SSH keys...";
1160
1159
  const platformKeys = await this.getPlatformKeys();
1161
1160
  const gateway = await this.platformClient.getGateway(location);
1162
1161
  const privateSubnetId = gateway?.subnetId;
1163
- spinner.text = `Creating VM '${vmName}'...`;
1164
- const vm = await this.platformClient.createVM({
1165
- name: vmName,
1162
+ spinner.text = "Creating VM...";
1163
+ const vm = await this.platformClient.createAppVM(appName, {
1166
1164
  location,
1167
1165
  size: vmSize,
1168
1166
  storage_size: storageSize,
@@ -1171,9 +1169,11 @@ var ClusterManager = class {
1171
1169
  public_key: platformKeys.platformPublicKey,
1172
1170
  enable_ip4: !privateSubnetId,
1173
1171
  // IPv6-only if we have a subnet for NAT64
1174
- private_subnet_id: privateSubnetId
1172
+ private_subnet_id: privateSubnetId,
1173
+ isPrimary: true
1175
1174
  });
1176
- spinner.text = `Waiting for VM to be ready...`;
1175
+ const vmName = vm.name;
1176
+ spinner.text = `Waiting for VM '${vmName}' to be ready...`;
1177
1177
  spinner.stop();
1178
1178
  console.log(chalk4.cyan("\nWaiting for VM to get an IPv6 address..."));
1179
1179
  const vmWithIp = await this.waitForVM(location, vmName, 600, false);
@@ -1226,23 +1226,23 @@ var ClusterManager = class {
1226
1226
  }
1227
1227
  const gateway = await this.platformClient.getGateway(cluster.location);
1228
1228
  const privateSubnetId = gateway?.subnetId;
1229
- const vmName = this.generateNodeName(appName, cluster.nodes);
1230
- let spinner = ora2(`Adding node '${vmName}' to cluster...`).start();
1229
+ let spinner = ora2("Adding node to cluster...").start();
1231
1230
  try {
1232
1231
  spinner.text = "Fetching platform SSH keys...";
1233
1232
  const platformKeys = await this.getPlatformKeys();
1234
- spinner.text = `Creating VM '${vmName}'...`;
1235
- const vm = await this.platformClient.createVM({
1236
- name: vmName,
1233
+ spinner.text = "Creating VM...";
1234
+ const vm = await this.platformClient.createAppVM(appName, {
1237
1235
  location: cluster.location,
1238
1236
  size: vmSize,
1239
1237
  boot_image: bootImage,
1240
1238
  unix_user: "root",
1241
1239
  public_key: platformKeys.platformPublicKey,
1242
1240
  enable_ip4: !privateSubnetId,
1243
- private_subnet_id: privateSubnetId
1241
+ private_subnet_id: privateSubnetId,
1242
+ isPrimary: false
1244
1243
  });
1245
- spinner.text = `Waiting for VM to be ready...`;
1244
+ const vmName = vm.name;
1245
+ spinner.text = `Waiting for VM '${vmName}' to be ready...`;
1246
1246
  spinner.stop();
1247
1247
  console.log(chalk4.cyan("\nWaiting for VM to get an IPv6 address..."));
1248
1248
  const vmWithIp = await this.waitForVM(cluster.location, vmName, 600, false);
@@ -1521,24 +1521,6 @@ The VM may still be provisioning. You can:
1521
1521
  }
1522
1522
  console.log(chalk4.yellow(" Warning: Timeout waiting for unregistry, continuing anyway..."));
1523
1523
  }
1524
- /**
1525
- * Generate a sequential node name: appName-1, appName-2, etc.
1526
- * Finds the next available number by checking existing node names
1527
- */
1528
- generateNodeName(appName, existingNodes) {
1529
- const existingNumbers = existingNodes.map((n) => {
1530
- const match = n.name.match(new RegExp(`^${appName}-(\\d+)$`));
1531
- return match ? parseInt(match[1], 10) : 0;
1532
- }).filter((n) => n > 0);
1533
- const maxNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) : 0;
1534
- return `${appName}-${maxNumber + 1}`;
1535
- }
1536
- /**
1537
- * Generate a random ID (used for legacy naming or fallback)
1538
- */
1539
- generateId() {
1540
- return Math.random().toString(36).substring(2, 9);
1541
- }
1542
1524
  /**
1543
1525
  * Sleep for specified milliseconds
1544
1526
  */
@@ -1956,6 +1938,19 @@ var PlatformClient = class {
1956
1938
  const { vm } = await this.request("POST", "/api/vms", params, { operation: `Create VM '${params.name}'` });
1957
1939
  return vm;
1958
1940
  }
1941
+ /**
1942
+ * Create a new VM for an app with auto-generated name
1943
+ * Name format: u{userId}-{appName}-{nodeNum}
1944
+ */
1945
+ async createAppVM(appName, params) {
1946
+ const { vm } = await this.request(
1947
+ "POST",
1948
+ `/api/apps/${appName}/vms`,
1949
+ params,
1950
+ { operation: `Create VM for app '${appName}'` }
1951
+ );
1952
+ return vm;
1953
+ }
1959
1954
  /**
1960
1955
  * Get a specific VM
1961
1956
  */
@@ -3916,8 +3911,75 @@ Error: ${error.message}
3916
3911
  return cmd;
3917
3912
  }
3918
3913
 
3914
+ // src/commands/services.ts
3915
+ import { Command as Command11 } from "commander";
3916
+ import chalk17 from "chalk";
3917
+ import ora11 from "ora";
3918
+ function createServicesCommand() {
3919
+ const cmd = new Command11("services");
3920
+ cmd.description("List services and their status for an app").argument("[app]", "App name (uses hackerrun.yaml if not specified)").action(async (appArg) => {
3921
+ const platformToken = getPlatformToken();
3922
+ const platformClient = new PlatformClient(platformToken);
3923
+ const uncloudRunner = new UncloudRunner(platformClient);
3924
+ try {
3925
+ const appName = appArg || getAppName();
3926
+ const app = await platformClient.getApp(appName);
3927
+ if (!app) {
3928
+ console.log(chalk17.red(`
3929
+ App '${appName}' not found
3930
+ `));
3931
+ process.exit(1);
3932
+ }
3933
+ const spinner = ora11("Fetching services...").start();
3934
+ try {
3935
+ const output = await uncloudRunner.serviceList(appName);
3936
+ spinner.stop();
3937
+ console.log(chalk17.cyan(`
3938
+ Services in '${appName}':
3939
+ `));
3940
+ const lines = output.trim().split("\n");
3941
+ if (lines.length === 0 || lines.length === 1 && !lines[0].trim()) {
3942
+ console.log(chalk17.yellow(" No services found.\n"));
3943
+ console.log(`Run ${chalk17.bold("hackerrun deploy")} to deploy your app.
3944
+ `);
3945
+ return;
3946
+ }
3947
+ for (const line of lines) {
3948
+ const trimmed = line.trim();
3949
+ if (trimmed.startsWith("Connecting") || trimmed.startsWith("Connected")) {
3950
+ continue;
3951
+ }
3952
+ if (trimmed) {
3953
+ if (trimmed.startsWith("NAME")) {
3954
+ console.log(chalk17.bold(` ${trimmed}`));
3955
+ } else {
3956
+ console.log(` ${trimmed}`);
3957
+ }
3958
+ }
3959
+ }
3960
+ console.log();
3961
+ console.log(chalk17.dim(`Machines: ${app.nodes.length}`));
3962
+ console.log(chalk17.dim(`Scale a service: hackerrun scale ${appName} <service> <replicas>`));
3963
+ console.log();
3964
+ } catch (error) {
3965
+ spinner.fail("Failed to fetch services");
3966
+ console.log(chalk17.yellow("\nMake sure the app is deployed and running.\n"));
3967
+ process.exit(1);
3968
+ }
3969
+ } catch (error) {
3970
+ console.error(chalk17.red(`
3971
+ Error: ${error.message}
3972
+ `));
3973
+ process.exit(1);
3974
+ } finally {
3975
+ uncloudRunner.cleanup();
3976
+ }
3977
+ });
3978
+ return cmd;
3979
+ }
3980
+
3919
3981
  // src/index.ts
3920
- var program = new Command11();
3982
+ var program = new Command12();
3921
3983
  program.name("hackerrun").description("Deploy apps with full control over your infrastructure").version("0.1.0");
3922
3984
  program.addCommand(createLoginCommand());
3923
3985
  program.addCommand(createConfigCommand());
@@ -3929,6 +3991,7 @@ program.addCommand(createEnvCommand());
3929
3991
  program.addCommand(createBuildsCommand());
3930
3992
  program.addCommand(createVPNCommands());
3931
3993
  program.addCommand(createScaleCommand());
3994
+ program.addCommand(createServicesCommand());
3932
3995
  var { appsCmd, nodesCmd, sshCmd, destroyCmd, linkCmd, renameCmd, domainCmd } = createAppCommands();
3933
3996
  program.addCommand(appsCmd);
3934
3997
  program.addCommand(nodesCmd);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hackerrun",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
4
4
  "description": "CLI tool to create and manage VMs using Ubicloud API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,84 @@
1
+ import { Command } from 'commander';
2
+ import chalk from 'chalk';
3
+ import ora from 'ora';
4
+ import { getPlatformToken } from '../lib/platform-auth.js';
5
+ import { PlatformClient } from '../lib/platform-client.js';
6
+ import { UncloudRunner } from '../lib/uncloud-runner.js';
7
+ import { getAppName } from '../lib/app-config.js';
8
+
9
+ export function createServicesCommand() {
10
+ const cmd = new Command('services');
11
+
12
+ cmd
13
+ .description('List services and their status for an app')
14
+ .argument('[app]', 'App name (uses hackerrun.yaml if not specified)')
15
+ .action(async (appArg: string | undefined) => {
16
+ const platformToken = getPlatformToken();
17
+ const platformClient = new PlatformClient(platformToken);
18
+ const uncloudRunner = new UncloudRunner(platformClient);
19
+
20
+ try {
21
+ const appName = appArg || getAppName();
22
+
23
+ const app = await platformClient.getApp(appName);
24
+ if (!app) {
25
+ console.log(chalk.red(`\nApp '${appName}' not found\n`));
26
+ process.exit(1);
27
+ }
28
+
29
+ const spinner = ora('Fetching services...').start();
30
+
31
+ try {
32
+ // Get service list from uncloud (includes status, replicas, etc.)
33
+ const output = await uncloudRunner.serviceList(appName);
34
+ spinner.stop();
35
+
36
+ console.log(chalk.cyan(`\nServices in '${appName}':\n`));
37
+
38
+ // Parse and display the output nicely
39
+ const lines = output.trim().split('\n');
40
+
41
+ if (lines.length === 0 || (lines.length === 1 && !lines[0].trim())) {
42
+ console.log(chalk.yellow(' No services found.\n'));
43
+ console.log(`Run ${chalk.bold('hackerrun deploy')} to deploy your app.\n`);
44
+ return;
45
+ }
46
+
47
+ // Print the table output from uncloud
48
+ for (const line of lines) {
49
+ const trimmed = line.trim();
50
+ // Skip connection messages
51
+ if (trimmed.startsWith('Connecting') || trimmed.startsWith('Connected')) {
52
+ continue;
53
+ }
54
+ if (trimmed) {
55
+ // Highlight header row
56
+ if (trimmed.startsWith('NAME')) {
57
+ console.log(chalk.bold(` ${trimmed}`));
58
+ } else {
59
+ console.log(` ${trimmed}`);
60
+ }
61
+ }
62
+ }
63
+
64
+ console.log();
65
+ console.log(chalk.dim(`Machines: ${app.nodes.length}`));
66
+ console.log(chalk.dim(`Scale a service: hackerrun scale ${appName} <service> <replicas>`));
67
+ console.log();
68
+
69
+ } catch (error) {
70
+ spinner.fail('Failed to fetch services');
71
+ console.log(chalk.yellow('\nMake sure the app is deployed and running.\n'));
72
+ process.exit(1);
73
+ }
74
+
75
+ } catch (error) {
76
+ console.error(chalk.red(`\nError: ${(error as Error).message}\n`));
77
+ process.exit(1);
78
+ } finally {
79
+ uncloudRunner.cleanup();
80
+ }
81
+ });
82
+
83
+ return cmd;
84
+ }
package/src/index.ts CHANGED
@@ -9,6 +9,7 @@ import { createConnectCommand, createDisconnectCommand } from './commands/connec
9
9
  import { createBuildsCommand } from './commands/builds.js';
10
10
  import { createVPNCommands } from './commands/vpn.js';
11
11
  import { createScaleCommand } from './commands/scale.js';
12
+ import { createServicesCommand } from './commands/services.js';
12
13
 
13
14
  const program = new Command();
14
15
 
@@ -36,8 +37,9 @@ program.addCommand(createBuildsCommand());
36
37
  // Register VPN command
37
38
  program.addCommand(createVPNCommands());
38
39
 
39
- // Register scale command
40
+ // Register scale and services commands
40
41
  program.addCommand(createScaleCommand());
42
+ program.addCommand(createServicesCommand());
41
43
 
42
44
  const { appsCmd, nodesCmd, sshCmd, destroyCmd, linkCmd, renameCmd, domainCmd } = createAppCommands();
43
45
  program.addCommand(appsCmd);
@@ -62,10 +62,7 @@ export class ClusterManager {
62
62
  async initializeCluster(options: ClusterInitOptions): Promise<AppCluster> {
63
63
  const { appName, location, vmSize, storageSize, bootImage } = options;
64
64
 
65
- // First node is always appName-1
66
- const vmName = `${appName}-1`;
67
-
68
- let spinner = ora(`Creating VM '${vmName}' in ${location}...`).start();
65
+ let spinner = ora(`Creating VM for '${appName}' in ${location}...`).start();
69
66
 
70
67
  try {
71
68
  // Get platform SSH keys (for VM creation)
@@ -77,10 +74,9 @@ export class ClusterManager {
77
74
  const privateSubnetId = gateway?.subnetId;
78
75
 
79
76
  // Create IPv6-only VM via platform API with platform SSH key
80
- // VM is placed in the same private subnet as the gateway for NAT64 routing
81
- spinner.text = `Creating VM '${vmName}'...`;
82
- const vm = await this.platformClient.createVM({
83
- name: vmName,
77
+ // VM name is auto-generated by platform: u{userId}-{appName}-{nodeNum}
78
+ spinner.text = 'Creating VM...';
79
+ const vm = await this.platformClient.createAppVM(appName, {
84
80
  location,
85
81
  size: vmSize,
86
82
  storage_size: storageSize,
@@ -89,9 +85,12 @@ export class ClusterManager {
89
85
  public_key: platformKeys.platformPublicKey,
90
86
  enable_ip4: !privateSubnetId, // IPv6-only if we have a subnet for NAT64
91
87
  private_subnet_id: privateSubnetId,
88
+ isPrimary: true,
92
89
  });
93
90
 
94
- spinner.text = `Waiting for VM to be ready...`;
91
+ const vmName = vm.name; // Name generated by platform
92
+
93
+ spinner.text = `Waiting for VM '${vmName}' to be ready...`;
95
94
  spinner.stop();
96
95
 
97
96
  console.log(chalk.cyan('\nWaiting for VM to get an IPv6 address...'));
@@ -172,10 +171,7 @@ export class ClusterManager {
172
171
  const gateway = await this.platformClient.getGateway(cluster.location);
173
172
  const privateSubnetId = gateway?.subnetId;
174
173
 
175
- // Generate sequential node name: appName-1, appName-2, etc.
176
- const vmName = this.generateNodeName(appName, cluster.nodes);
177
-
178
- let spinner = ora(`Adding node '${vmName}' to cluster...`).start();
174
+ let spinner = ora('Adding node to cluster...').start();
179
175
 
180
176
  try {
181
177
  // Get platform SSH keys (for VM creation)
@@ -183,9 +179,9 @@ export class ClusterManager {
183
179
  const platformKeys = await this.getPlatformKeys();
184
180
 
185
181
  // Create new IPv6-only VM via platform API with platform SSH key
186
- spinner.text = `Creating VM '${vmName}'...`;
187
- const vm = await this.platformClient.createVM({
188
- name: vmName,
182
+ // VM name is auto-generated by platform: u{userId}-{appName}-{nodeNum}
183
+ spinner.text = 'Creating VM...';
184
+ const vm = await this.platformClient.createAppVM(appName, {
189
185
  location: cluster.location,
190
186
  size: vmSize,
191
187
  boot_image: bootImage,
@@ -193,9 +189,12 @@ export class ClusterManager {
193
189
  public_key: platformKeys.platformPublicKey,
194
190
  enable_ip4: !privateSubnetId,
195
191
  private_subnet_id: privateSubnetId,
192
+ isPrimary: false,
196
193
  });
197
194
 
198
- spinner.text = `Waiting for VM to be ready...`;
195
+ const vmName = vm.name; // Name generated by platform
196
+
197
+ spinner.text = `Waiting for VM '${vmName}' to be ready...`;
199
198
  spinner.stop();
200
199
  console.log(chalk.cyan('\nWaiting for VM to get an IPv6 address...'));
201
200
  const vmWithIp = await this.waitForVM(cluster.location, vmName, 600, false);
@@ -544,31 +543,6 @@ REMOTESCRIPT`, {
544
543
  console.log(chalk.yellow(' Warning: Timeout waiting for unregistry, continuing anyway...'));
545
544
  }
546
545
 
547
- /**
548
- * Generate a sequential node name: appName-1, appName-2, etc.
549
- * Finds the next available number by checking existing node names
550
- */
551
- private generateNodeName(appName: string, existingNodes: VMNode[]): string {
552
- // Extract existing numbers from node names
553
- const existingNumbers = existingNodes
554
- .map(n => {
555
- const match = n.name.match(new RegExp(`^${appName}-(\\d+)$`));
556
- return match ? parseInt(match[1], 10) : 0;
557
- })
558
- .filter(n => n > 0);
559
-
560
- // Find the next available number
561
- const maxNumber = existingNumbers.length > 0 ? Math.max(...existingNumbers) : 0;
562
- return `${appName}-${maxNumber + 1}`;
563
- }
564
-
565
- /**
566
- * Generate a random ID (used for legacy naming or fallback)
567
- */
568
- private generateId(): string {
569
- return Math.random().toString(36).substring(2, 9);
570
- }
571
-
572
546
  /**
573
547
  * Sleep for specified milliseconds
574
548
  */
@@ -64,6 +64,19 @@ export interface CreateVMParams {
64
64
  private_subnet_id?: string; // Put VM in specific private subnet for NAT64 routing
65
65
  }
66
66
 
67
+ // Parameters for creating an app VM (name is auto-generated by platform)
68
+ export interface CreateAppVMParams {
69
+ location: string;
70
+ size: string;
71
+ storage_size?: number;
72
+ unix_user: string;
73
+ public_key: string;
74
+ boot_image: string;
75
+ enable_ip4?: boolean;
76
+ private_subnet_id?: string;
77
+ isPrimary?: boolean;
78
+ }
79
+
67
80
  export interface UbicloudVM {
68
81
  id: string;
69
82
  name: string;
@@ -274,6 +287,20 @@ export class PlatformClient {
274
287
  return vm;
275
288
  }
276
289
 
290
+ /**
291
+ * Create a new VM for an app with auto-generated name
292
+ * Name format: u{userId}-{appName}-{nodeNum}
293
+ */
294
+ async createAppVM(appName: string, params: CreateAppVMParams): Promise<UbicloudVM> {
295
+ const { vm } = await this.request<{ vm: UbicloudVM }>(
296
+ 'POST',
297
+ `/api/apps/${appName}/vms`,
298
+ params,
299
+ { operation: `Create VM for app '${appName}'` }
300
+ );
301
+ return vm;
302
+ }
303
+
277
304
  /**
278
305
  * Get a specific VM
279
306
  */