@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.
- package/README.md +35 -8
- package/dist/index.js +73 -21
- 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 --
|
|
40
|
-
hyze
|
|
41
|
-
hyze
|
|
42
|
-
hyze
|
|
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>`, `--
|
|
418
|
-
|
|
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
|
-
|
|
424
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|