@hyze-cloud/cli 0.1.0 → 0.2.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.
Files changed (3) hide show
  1. package/README.md +35 -8
  2. package/dist/index.js +73 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -36,12 +36,18 @@ sudo mv dist/hyze /usr/local/bin/hyze
36
36
  ```bash
37
37
  hyze login # paste a key created in the dashboard (Settings → Developer)
38
38
  hyze projects # what exists in this workspace
39
- hyze deploy . --name my-api --runtime bun --port 3000 --subdomain my-api.hyzecloud.app
40
- hyze status my-api # is it up? unknown when the platform cannot see the container
41
- hyze deployments my-api # build history: queue place, waiting reason, cause of death
42
- hyze logs my-api # container output; `hyze logs my-api <deploymentId>` for the build log
39
+ hyze deploy . --name my-api --subdomain my-api.hyzecloud.app
40
+ hyze link # bind this folder to the app
41
+ hyze status # is it up? unknown when the platform cannot see the container
42
+ hyze deployments # build history: queue place, waiting reason, cause of death
43
+ hyze logs # container output; pass a <deploymentId> for the build log
43
44
  ```
44
45
 
46
+ `--name` is the only required flag on a first deploy: the runtime, the port and
47
+ the memory come from inspecting what you sent. Add `--subdomain` when the app
48
+ should be reachable on a public URL — an app the detector reads as an HTTP server
49
+ needs one, and refuses before uploading if you forget it.
50
+
45
51
  ## Commands
46
52
 
47
53
  | Command | Talks to | What it does |
@@ -49,6 +55,7 @@ hyze logs my-api # container output; `hyze logs my-api <deploym
49
55
  | `login [--key <key>] [--no-verify]` | `GET /apps/`, `GET /plans/current` | store an API key, after checking it against the API |
50
56
  | `logout` | — | remove the stored key of the active profile |
51
57
  | `projects [--query] [--page] [--limit]` | `GET /apps/` | list the workspace's projects |
58
+ | `link [appId] [--print]` | `GET /apps/` | bind this directory to a project (`.hyzerc.json`) |
52
59
  | `deploy [path] [--name] [--app] [--repo] …` | `GET /platform/config`, `POST /apps/inspect-env`, `POST /apps/deploy`, `GET /apps/:appId/deployments/:id` | zip (or send a `.zip`), upload, follow the build |
53
60
  | `deployments [appId] [deploymentId]` | `GET /apps/:appId/deployments[/:id]` | build history, or one deployment with its stage timeline |
54
61
  | `logs [appId] [deploymentId]` | `GET /apps/:appId/logs`, `GET /apps/:appId/deployments/:id` | container output, or the persisted build log |
@@ -58,6 +65,10 @@ hyze logs my-api # container output; `hyze logs my-api <deploym
58
65
  to the directory, so inside a project `hyze status`, `hyze logs` and
59
66
  `hyze deploy .` need no arguments.
60
67
 
68
+ > The argument is the project's **id**, not its name. Inside a bound directory
69
+ > you never type either; outside one, `hyze projects` lists the ids and
70
+ > `hyze link <appId>` binds the directory.
71
+
61
72
  ```json
62
73
  // .hyzerc.json — nearest ancestor wins
63
74
  { "appId": "app_abc123", "workspaceId": "org_abc", "apiUrl": "https://api.hyzecloud.com/api" }
@@ -414,14 +425,26 @@ created status deployment queue waiting
414
425
  ## Deploy details
415
426
 
416
427
  Key flags: `--name`, `--app <appId>` (redeploy, defaults to the project in
417
- `.hyzerc.json`), `--runtime node|bun|python`, `--memory <mb>`, `--port`/`--subdomain`
418
- (must be passed together), `--env KEY=VALUE` (repeatable), `--env-file`,
428
+ `.hyzerc.json`), `--runtime node|bun|python`, `--memory <mb>`, `--subdomain <host>`,
429
+ `--port <n>`, `--env KEY=VALUE` (repeatable), `--env-file`,
419
430
  `--root-dir`, `--start-command`, `--install-command`, `--build-command`,
420
431
  `--app-type`, `--machine`, `--exclude`/`--include`/`--include-ignored`,
421
432
  `--no-wait`, and `--repo <owner/name> [--branch] [--auto-deploy]` for GitHub.
422
433
 
423
- When `--runtime` is omitted the CLI asks the API to inspect the upload and uses the
424
- detected runtime. With waiting enabled (default) it follows the deployment stage
434
+ What you do not have to pass is what the platform can work out. Without
435
+ `--runtime` the CLI asks the API to inspect the upload and uses the detected
436
+ runtime. Without `--port` it uses the port that inspection found, so
437
+ `--subdomain` alone publishes an HTTP app. Without `--memory` it asks for the
438
+ memory the project needs, capped by what the plan can still hand out — and says
439
+ so when the cap bites.
440
+
441
+ **A redeploy keeps what you did not mention.** Inside a bound directory,
442
+ `hyze deploy .` targets that app and leaves its public address, port and memory
443
+ exactly as they were: a field absent from the request means "keep it", never
444
+ "clear it". Pass the flag to change it (`--subdomain other.hyzecloud.app`,
445
+ `--memory 512`).
446
+
447
+ With waiting enabled (default) it follows the deployment stage
425
448
  timeline on stderr and prints the settled deployment on stdout; a failed deployment
426
449
  exits `1` with the API error and a pointer to `hyze logs <appId> <deploymentId>`.
427
450
 
@@ -570,6 +593,10 @@ browser and an action that failed — and `tui.e2e.test.ts` runs the real binary
570
593
  inside a PTY to check what the terminal ends up showing, including the lifecycle
571
594
  calls it really sent and the cursor being handed back on exit.
572
595
 
596
+ ## Changelog
597
+
598
+ See [CHANGELOG.md](https://github.com/Hyze-Cloud/hyze-cloud/blob/main/apps/cli/CHANGELOG.md).
599
+
573
600
  ## License
574
601
 
575
602
  MIT
package/dist/index.js CHANGED
@@ -3652,7 +3652,7 @@ function deployPayload(name, flags, runtime, options) {
3652
3652
  ...flags.appId ? { appId: flags.appId } : {},
3653
3653
  name,
3654
3654
  runtime,
3655
- memoryMB: flags.memoryMB,
3655
+ ...flags.memoryMB !== undefined ? { memoryMB: flags.memoryMB } : {},
3656
3656
  ...typeof options.startCommand === "string" ? { startupCommand: options.startCommand } : {},
3657
3657
  ...typeof options.installCommand === "string" ? { installCommand: options.installCommand } : {},
3658
3658
  ...typeof options.buildCommand === "string" ? { buildCommand: options.buildCommand } : {},
@@ -3733,6 +3733,7 @@ function reportMorePages(output, data) {
3733
3733
  }
3734
3734
 
3735
3735
  // src/commands/deploy.ts
3736
+ var MIN_MEMORY_MB = 256;
3736
3737
  var RUNTIMES = { node: "node", bun: "bun", python: "python" };
3737
3738
  var APP_TYPES = {
3738
3739
  "web-service": "web-service",
@@ -3790,12 +3791,9 @@ function resolveAppType(value) {
3790
3791
  return appType;
3791
3792
  }
3792
3793
  function parseDeployFlags(options, boundAppId) {
3793
- const memoryMB = parseInteger(String(options.memory), "--memory", 1);
3794
+ const memoryMB = options.memory === undefined ? undefined : parseInteger(String(options.memory), "--memory", 1);
3794
3795
  const exposePort = options.port === undefined ? undefined : parseInteger(String(options.port), "--port", 1, 65535);
3795
3796
  const subdomain = typeof options.subdomain === "string" ? options.subdomain : undefined;
3796
- if (exposePort === undefined !== (subdomain === undefined)) {
3797
- throw new UsageError("--port and --subdomain must be provided together");
3798
- }
3799
3797
  return {
3800
3798
  memoryMB,
3801
3799
  exposePort,
@@ -3806,6 +3804,47 @@ function parseDeployFlags(options, boundAppId) {
3806
3804
  repo: typeof options.repo === "string" ? options.repo : undefined
3807
3805
  };
3808
3806
  }
3807
+ function assertExposurePairing(exposePort, subdomain, detectedPort) {
3808
+ if (exposePort === undefined === (subdomain === undefined))
3809
+ return;
3810
+ if (subdomain !== undefined) {
3811
+ throw new UsageError("--subdomain needs a port, and none could be detected", {
3812
+ hint: "Pass --port <n> — the port the app listens on."
3813
+ });
3814
+ }
3815
+ if (detectedPort !== undefined) {
3816
+ throw new UsageError(`This app serves HTTP on port ${detectedPort}, so it needs a public hostname`, {
3817
+ hint: "Pass --subdomain <host>, for example my-app.hyzecloud.app — or --app-type bot if it is not a web app."
3818
+ });
3819
+ }
3820
+ throw new UsageError("--port needs a public hostname", {
3821
+ hint: "Pass --subdomain <host>, for example my-app.hyzecloud.app."
3822
+ });
3823
+ }
3824
+ async function fetchAvailableMemory(client) {
3825
+ try {
3826
+ const plan = await client.get("/plans/current");
3827
+ const available = plan.usage?.availableRAM;
3828
+ return typeof available === "number" ? available : null;
3829
+ } catch {
3830
+ return null;
3831
+ }
3832
+ }
3833
+ async function resolveDeployMemory(client, flags, detectedMemoryMB, output) {
3834
+ if (flags.memoryMB !== undefined)
3835
+ return flags.memoryMB;
3836
+ if (flags.appId !== undefined)
3837
+ return;
3838
+ const wanted = detectedMemoryMB ?? MIN_MEMORY_MB;
3839
+ const available = await fetchAvailableMemory(client);
3840
+ if (available === null)
3841
+ return wanted;
3842
+ const capped = Math.max(MIN_MEMORY_MB, Math.min(wanted, available));
3843
+ if (capped !== wanted) {
3844
+ output.info(`memory: ${capped}MB — the plan has ${available}MB free (detected ${wanted}MB)`);
3845
+ }
3846
+ return capped;
3847
+ }
3809
3848
  async function resolveAppName(ctx, flags, options, fallbackName) {
3810
3849
  const explicit = typeof options.name === "string" ? options.name.trim() : "";
3811
3850
  if (explicit.length > 0)
@@ -3923,6 +3962,30 @@ async function reportDeploy(ctx, data, options, appName) {
3923
3962
  output.warn(cancelledLine(appName, status));
3924
3963
  }
3925
3964
  }
3965
+ function resolveDetectedRuntime(runtimeFlag, detected, output) {
3966
+ if (runtimeFlag)
3967
+ return runtimeFlag;
3968
+ if (!detected?.runtime) {
3969
+ throw new UsageError("Could not detect the runtime — pass --runtime node|bun|python", {
3970
+ hint: `Detected files: ${detected?.detectedFiles.join(", ") || "none"}`
3971
+ });
3972
+ }
3973
+ output.info(detectionLine(detected.runtime, detected.label, detected.confidence));
3974
+ return detected.runtime;
3975
+ }
3976
+ async function resolveDeployInputs(ctx, options, flags, upload, rootDirectory) {
3977
+ const { client, output } = ctx;
3978
+ const runtimeFlag = resolveRuntime(options.runtime);
3979
+ const wantsDetection = runtimeFlag === undefined || flags.exposePort === undefined;
3980
+ const detected = wantsDetection ? await client.post("/apps/inspect-env", {
3981
+ form: multipartBody(upload, rootDirectory)
3982
+ }) : undefined;
3983
+ const runtime = resolveDetectedRuntime(runtimeFlag, detected, output);
3984
+ const exposePort = flags.exposePort ?? detected?.exposePort ?? undefined;
3985
+ assertExposurePairing(exposePort, flags.subdomain, detected?.exposePort ?? undefined);
3986
+ const memoryMB = await resolveDeployMemory(client, flags, detected?.memoryMB, output);
3987
+ return { runtime, exposePort, memoryMB };
3988
+ }
3926
3989
  async function deployFromUpload(ctx, options, flags, pathArgument) {
3927
3990
  const { client, output } = ctx;
3928
3991
  const name = await resolveAppName(ctx, flags, options, undefined);
@@ -3930,22 +3993,10 @@ async function deployFromUpload(ctx, options, flags, pathArgument) {
3930
3993
  const limit = await fetchUploadLimit(client);
3931
3994
  const upload = buildUpload({ source, flags: options, limit, onInfo: (message) => output.info(message) });
3932
3995
  const rootDirectory = typeof options.rootDir === "string" ? { rootDirectory: options.rootDir } : {};
3933
- let runtime = resolveRuntime(options.runtime);
3934
- if (!runtime) {
3935
- const detected = await client.post("/apps/inspect-env", {
3936
- form: multipartBody(upload, rootDirectory)
3937
- });
3938
- if (!detected.runtime) {
3939
- throw new UsageError("Could not detect the runtime — pass --runtime node|bun|python", {
3940
- hint: `Detected files: ${detected.detectedFiles.join(", ") || "none"}`
3941
- });
3942
- }
3943
- runtime = detected.runtime;
3944
- output.info(detectionLine(runtime, detected.label, detected.confidence));
3945
- }
3996
+ const { runtime, exposePort, memoryMB } = await resolveDeployInputs(ctx, options, flags, upload, rootDirectory);
3946
3997
  const data = await client.post("/apps/deploy", {
3947
3998
  form: multipartBody(upload, {
3948
- ...deployPayload(name, flags, runtime, options),
3999
+ ...deployPayload(name, { ...flags, exposePort, memoryMB }, runtime, options),
3949
4000
  ...Object.keys(flags.env).length > 0 ? { envVars: JSON.stringify(flags.env) } : {},
3950
4001
  ...options.autoRestart === false ? { autoRestart: false } : {}
3951
4002
  })
@@ -3977,11 +4028,12 @@ async function deployFromRepository(ctx, options, flags) {
3977
4028
  if (!runtime) {
3978
4029
  throw new UsageError("--runtime is required for repository deploys", { hint: "Pass --runtime node|bun|python." });
3979
4030
  }
4031
+ assertExposurePairing(flags.exposePort, flags.subdomain, undefined);
3980
4032
  const name = await resolveAppName(ctx, flags, options, repoName);
3981
4033
  const repository = await resolveRepository(ctx, owner, repoName, typeof options.branch === "string" ? options.branch : undefined);
3982
4034
  const data = await ctx.client.post("/apps/deploy-from-repo", {
3983
4035
  body: {
3984
- ...deployPayload(name, flags, runtime, options),
4036
+ ...deployPayload(name, { ...flags, memoryMB: flags.memoryMB ?? 512 }, runtime, options),
3985
4037
  ...Object.keys(flags.env).length > 0 ? { envVars: flags.env } : {},
3986
4038
  ...options.autoDeploy === true ? { autoDeploy: true } : {},
3987
4039
  repository: { id: repository.id, owner, name: repoName, branch: repository.branch }
@@ -3990,7 +4042,7 @@ async function deployFromRepository(ctx, options, flags) {
3990
4042
  await reportDeploy(ctx, data, options, name);
3991
4043
  }
3992
4044
  function registerDeployCommands(program) {
3993
- program.command("deploy [path]").description("Deploy a directory, a ZIP or a GitHub repository (defaults to the current directory)").option("--zip <file>", "deploy a ZIP file instead of a directory").option("--repo <owner/name>", "deploy from a GitHub repository").option("--branch <branch>", "branch for --repo (defaults to the repository default branch)").option("--auto-deploy", "redeploy automatically on push (--repo only)").option("--name <name>", "app name (required for a new app)").option("--app <appId>", "redeploy an existing app (defaults to the project in .hyzerc.json)").option("--runtime <runtime>", "node | bun | python (detected from the project when omitted)").option("--memory <mb>", "memory in MB", "512").option("--port <n>", "public port (must be paired with --subdomain)").option("--subdomain <host>", "public hostname (must be paired with --port)").option("--env <pairs...>", "environment variables as KEY=VALUE (repeatable)").option("--env-file <file>", "read environment variables from a .env-style file").option("--root-dir <dir>", "build from a subdirectory of the project").option("--start-command <cmd>", "override the start command").option("--install-command <cmd>", "override the install command").option("--build-command <cmd>", "override the build command").option("--app-type <type>", "web-service | static-site | bot | generic").option("--machine <machineId>", "pin the app to a specific machine").option("--no-auto-restart", "never restart the app automatically after a crash").option("--exclude <patterns...>", "ignore extra file patterns when zipping").option("--include <patterns...>", "re-include patterns that an ignore rule excluded").option("--include-ignored", "upload ignored files too (never .git or node_modules)").option("--wait", "wait for the deployment to finish (default)").option("--no-wait", "return as soon as the deployment is queued").action(async (path, options, command) => {
4045
+ program.command("deploy [path]").description("Deploy a directory, a ZIP or a GitHub repository (defaults to the current directory)").option("--zip <file>", "deploy a ZIP file instead of a directory").option("--repo <owner/name>", "deploy from a GitHub repository").option("--branch <branch>", "branch for --repo (defaults to the repository default branch)").option("--auto-deploy", "redeploy automatically on push (--repo only)").option("--name <name>", "app name (required for a new app)").option("--app <appId>", "redeploy an existing app (defaults to the project in .hyzerc.json)").option("--runtime <runtime>", "node | bun | python (detected from the project when omitted)").option("--memory <mb>", "memory in MB (default: what the app needs, capped by the plan)").option("--port <n>", "public port (default: the port the app is detected to serve on)").option("--subdomain <host>", "public hostname (required to expose the app)").option("--env <pairs...>", "environment variables as KEY=VALUE (repeatable)").option("--env-file <file>", "read environment variables from a .env-style file").option("--root-dir <dir>", "build from a subdirectory of the project").option("--start-command <cmd>", "override the start command").option("--install-command <cmd>", "override the install command").option("--build-command <cmd>", "override the build command").option("--app-type <type>", "web-service | static-site | bot | generic").option("--machine <machineId>", "pin the app to a specific machine").option("--no-auto-restart", "never restart the app automatically after a crash").option("--exclude <patterns...>", "ignore extra file patterns when zipping").option("--include <patterns...>", "re-include patterns that an ignore rule excluded").option("--include-ignored", "upload ignored files too (never .git or node_modules)").option("--wait", "wait for the deployment to finish (default)").option("--no-wait", "return as soon as the deployment is queued").action(async (path, options, command) => {
3994
4046
  const ctx = createContext(command);
3995
4047
  const flags = parseDeployFlags(options, typeof options.app === "string" ? options.app : ctx.settings.projectConfig.appId);
3996
4048
  if (flags.repo) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyze-cloud/cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Command-line interface for Hyze Cloud \u2014 deploy projects, follow builds and read logs.",
5
5
  "type": "module",
6
6
  "bin": {