Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
This repository holds the two public npm packages of AntelopeJS Cloud:

- `@antelopejs/cloud-sdk` in [`sdk/`](sdk/README.md) is a typed client for the AntelopeJS Cloud API. Its types are generated from the OpenAPI spec in `sdk/openapi/`, which the package also ships.
- `@antelopejs/cloud` in [`cli/`](cli/README.md) is the `ajs-cloud` command-line client. It deploys a folder to AntelopeJS Cloud and operates it, through the SDK.
- `@antelopejs/cloud` in [`cli/`](cli/README.md) is the command-line client, run as `ajs cloud`. It deploys a folder to AntelopeJS Cloud and operates it, through the SDK.

Both need Node.js 20.19 or later.

## Install

Install the CLI globally to get the `ajs-cloud` command:
Install the CLI globally to get the `ajs cloud` command, a plugin of the `ajs` command of `@antelopejs/core`:

```sh
npm install -g @antelopejs/cloud
Expand All @@ -26,10 +26,10 @@ npm install @antelopejs/cloud-sdk
Log in once, then deploy from the folder of your app:

```sh
ajs-cloud login
ajs cloud login
cd my-app
ajs-cloud deploy
ajs-cloud status
ajs cloud deploy
ajs cloud status
```

The first `deploy` creates the project and its service, then prints the public URL. The [CLI README](cli/README.md) lists every command and option.
Expand All @@ -41,9 +41,9 @@ import { CloudApi, CloudApiError } from "@antelopejs/cloud-sdk";

const api = new CloudApi({ token: process.env.ANTELOPE_CLOUD_TOKEN! });
try {
console.log(await api.listProjects());
console.log(await api.listProjects());
} catch (error) {
if (error instanceof CloudApiError) console.error(error.status, error.code);
if (error instanceof CloudApiError) console.error(error.status, error.code);
}
```

Expand All @@ -62,8 +62,8 @@ const api = new CloudApi({ token, baseUrl: "https://api.example.test" });
To reach it from the CLI, pass `--api-url` or set `ANTELOPE_CLOUD_URL`. The flag wins over the variable. Log in against that API first, because a saved session belongs to the API it was opened on:

```sh
ajs-cloud login --api-url https://api.example.test
ANTELOPE_CLOUD_URL=https://api.example.test ajs-cloud status
ajs cloud login --api-url https://api.example.test
ANTELOPE_CLOUD_URL=https://api.example.test ajs cloud status
```

A command whose API differs from the one of the saved session stops before any request, with the error code `api_url_mismatch`.
Expand Down
75 changes: 41 additions & 34 deletions cli/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# `@antelopejs/cloud` CLI

`ajs-cloud` deploys a folder to AntelopeJS Cloud and operates it: log in once,
run `ajs-cloud deploy`, and get a public URL.
`ajs cloud` deploys a folder to AntelopeJS Cloud and operates it: log in once,
run `ajs cloud deploy`, and get a public URL.

## Install

Expand All @@ -14,18 +14,28 @@ npm install -g @antelopejs/cloud
# or
pnpm add -g @antelopejs/cloud

ajs-cloud --version
ajs cloud --version
```

To run it without installing, `npx` fetches the latest version on each call:
`ajs cloud` is a plugin of the `ajs` command of `@antelopejs/core`: `ajs`
runs the `ajs-cloud` binary this package installs, which it finds in the
nearest `node_modules/.bin`, then on the `PATH`. A machine without the `ajs`
command gets it with `npm install -g @antelopejs/core`. The binary can also
be run by its own name.

To run it without installing, `npx` fetches the latest version on each call.
Always give npx the scoped package name:

```sh
npx @antelopejs/cloud login
npx @antelopejs/cloud deploy
```

The binary is `ajs-cloud`. The package also exports a Commander command
factory, meant to be mounted as `ajs cloud` by `@antelopejs/core`:
The hints the CLI prints name the command the way it was run: `ajs cloud …`,
or `npx @antelopejs/cloud …` when npx runs it.

The package also exports a Commander command factory, for a program that
mounts the command itself:

```ts
import { mountCloudCommand } from "@antelopejs/cloud/command";
Expand All @@ -34,17 +44,14 @@ import { Command } from "commander";
mountCloudCommand(new Command("ajs"));
```

Released versions of core (1.4, 1.5) build the `ajs` command tree statically
and do not mount it yet: use `ajs-cloud` directly.

## Quick start

```sh
ajs-cloud login # approve the login in your browser
ajs cloud login # approve the login in your browser
cd my-app
ajs-cloud deploy # first deploy: creates the project and its service
ajs-cloud status
ajs-cloud logs web
ajs cloud deploy # first deploy: creates the project and its service
ajs cloud status
ajs cloud logs web
```

## Commands
Expand All @@ -61,23 +68,23 @@ ajs-cloud logs web
| `logs <service>` | Prints the service's recent logs (last hour); `-f` follows them |
| `rollback <service> [revision]` | Rolls a service back to its previous revision, or to the given one |

`ajs-cloud --help` and `ajs-cloud <command> --help` list every option, with
`ajs cloud --help` and `ajs cloud <command> --help` list every option, with
examples.

```sh
ajs-cloud link my-project --service api=apps/api --service app=apps/app
ajs-cloud link my-project --service web=. --service frontend=. --dockerfile frontend=frontend.Dockerfile # two services of one folder
ajs-cloud deploy # all services from the project root
ajs-cloud deploy api # one named service
cd apps/app && ajs-cloud deploy # inferred from the configured path
ajs-cloud logs api -f
ajs-cloud open [api]
ajs-cloud rollback api [revision-id]
ajs cloud link my-project --service api=apps/api --service app=apps/app
ajs cloud link my-project --service web=. --service frontend=. --dockerfile frontend=frontend.Dockerfile # two services of one folder
ajs cloud deploy # all services from the project root
ajs cloud deploy api # one named service
cd apps/app && ajs cloud deploy # inferred from the configured path
ajs cloud logs api -f
ajs cloud open [api]
ajs cloud rollback api [revision-id]
```

### First deploy

In a folder that is not linked yet, `ajs-cloud deploy` creates what it needs
In a folder that is not linked yet, `ajs cloud deploy` creates what it needs
instead of failing:

1. It checks that your workspace can deploy. A new workspace takes about a
Expand Down Expand Up @@ -116,7 +123,7 @@ ask on, `deploy` stops before any write with exit code 2 and names the flags
to pass:

```sh
ajs-cloud deploy --yes --project my-app --port 8080
ajs cloud deploy --yes --project my-app --port 8080
```

A project named with `--project` that already exists is deployed to as-is.
Expand All @@ -130,7 +137,7 @@ deploys from it use its own link. Leave the folder out of the parent's upload
with a `.cloudignore`:

```sh
cd frontend && ajs-cloud deploy frontend --yes --project my-app --port 3000
cd frontend && ajs cloud deploy frontend --yes --project my-app --port 3000
```

### Following a deployment
Expand All @@ -148,13 +155,13 @@ and ends with the total time and the public URL:
Deployed web · 1m 31s

Next steps
ajs-cloud status check where it is
ajs-cloud logs web -f read the service logs
ajs cloud status check where it is
ajs cloud logs web -f read the service logs
https://my-app.antelopejs.io
```

The progress and the summary go to stderr; the public URL is the result, on
stdout, so `URL=$(ajs-cloud deploy -q)` captures it.
stdout, so `URL=$(ajs cloud deploy -q)` captures it.

A new service's address is `<project>-<service>.antelopejs.io`, without
repeating what the project name already says: a service named `web`, `app` or
Expand All @@ -176,15 +183,15 @@ and a quiet phase is reported every 30 seconds
(`--timeout <minutes>` to change it). The deployment itself keeps running:
the command prints its console page and exits with an error.
- **Ctrl+C** stops the CLI, not the deployment, and exits with code 130. The
CLI says so and prints how to follow it: `ajs-cloud status`,
`ajs-cloud logs <service> -f`, and the deployment's console page.
CLI says so and prints how to follow it: `ajs cloud status`,
`ajs cloud logs <service> -f`, and the deployment's console page.
- `status` shows a deployment still on its way, for example
`web deploying (queued, position in queue: 1)` while it builds, or
`web deploying (rolling out)` once it is built.

## Output, languages and errors

`ajs-cloud` follows the output contract of `ajs` and its plugins:
`ajs cloud` follows the output contract of `ajs` and its plugins:

- **stdout carries results only**: tables (`status`), key/value lines
(`whoami`), log lines, URLs (`open`, `deploy`), help and `--version`.
Expand All @@ -197,7 +204,7 @@ and a quiet phase is reported every 30 seconds

```
✖ You are not logged in
→ Run ajs-cloud login, then run the command again
→ Run ajs cloud login, then run the command again
```

`--verbose` adds the stack trace of the underlying failure.
Expand All @@ -208,7 +215,7 @@ and a quiet phase is reported every 30 seconds
(`-y`), `--no-browser`, `--lang en|fr`, `--api-url`, `-q`/`--quiet` (results,
warnings and errors only), `--verbose[=channels]` and `--no-color`. They read
the same environment as `ajs`: `NO_COLOR`, `FORCE_COLOR`,
`ANTELOPEJS_QUIET`, `ANTELOPEJS_VERBOSE` and `CI`. `ajs-cloud help
`ANTELOPEJS_QUIET`, `ANTELOPEJS_VERBOSE` and `CI`. `ajs cloud help
environment` lists them.
- **Languages**: the messages of the Cloud CLI are in English or French
(`--lang`, else `LANG`). The texts shared with `ajs` (usage errors, help
Expand All @@ -234,7 +241,7 @@ the token itself stays valid until it expires or is revoked.
`--api-url` takes precedence over `ANTELOPE_CLOUD_URL`. Login uses that endpoint
and binds the saved session to it. Other commands use the saved endpoint when
neither is set. If an override differs from the saved endpoint, they stop before
any API request with `api_url_mismatch`; run `ajs-cloud login --api-url <url>`
any API request with `api_url_mismatch`; run `ajs cloud login --api-url <url>`
to authenticate with the intended endpoint first. The CLI never moves the saved
token to the override endpoint.

Expand Down
5 changes: 3 additions & 2 deletions cli/src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { consoleUrl } from "./console-urls.js";
import { CliError } from "./errors.js";
import { format, message } from "./i18n.js";
import { asInvoked } from "./invocation.js";
import { isNetworkFailure } from "./network.js";
import { feedback, record, result } from "./output.js";
import {
Expand Down Expand Up @@ -179,7 +180,7 @@ function reportLogin(
headline: message("loggedIn", options.lang),
nextSteps: [
{
command: "ajs-cloud deploy",
command: asInvoked("ajs-cloud deploy"),
description: message("loginNext", options.lang),
},
],
Expand Down Expand Up @@ -370,7 +371,7 @@ export async function rollback(
throw new CliError("rollback_revision_unavailable", serviceName);
const values = { service: serviceName, revision: String(revision.number) };
const prompter = runtime.prompter({
command: `ajs-cloud rollback ${serviceName}`,
command: asInvoked(`ajs-cloud rollback ${serviceName}`),
defaultsFlag: YES_FLAG,
});
const isConfirmed = await prompter.confirm({
Expand Down
3 changes: 2 additions & 1 deletion cli/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from "./error-messages.js";
import { isNumber, isRecord, isString } from "./guards.js";
import { ENVIRONMENT_TOPIC, formatEnvironmentHelp } from "./help.js";
import { commandName } from "./invocation.js";
import type { GlobalOptions } from "./types.js";

interface JsonError {
Expand Down Expand Up @@ -132,7 +133,7 @@ function exitOnBrokenPipe(): void {

const args = normalizeVerboseArguments(process.argv.slice(2));
const isJson = optionArguments(args).some((arg) => JSON_FLAGS.includes(arg));
const program = createCloudCommand().name("ajs-cloud");
const program = createCloudCommand().name(commandName());

function language(): string | undefined {
return (
Expand Down
36 changes: 28 additions & 8 deletions cli/src/command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { DEFAULT_DEPLOY_TIMEOUT_MINUTES } from "./constants.js";
import { isRecord, isString } from "./guards.js";
import { applyCloudHelp, ENVIRONMENT_TOPIC } from "./help.js";
import { language } from "./i18n.js";
import { asInvoked } from "./invocation.js";
import { defaultRuntime } from "./runtime.js";
import type {
DeployOptions,
Expand Down Expand Up @@ -46,6 +47,25 @@ function packageVersion(): string {
return manifest.version;
}

/** Examples with each command written the way the person runs it. */
function invoked(examples: HelpExample[]): HelpExample[] {
return examples.map((example) => ({
...example,
command: asInvoked(example.command),
}));
}

function invokedExamples(examples: HelpExample[], width: number): string {
return formatExamples(invoked(examples), width);
}

function withInvokedExamples(
command: Command,
examples: HelpExample[],
): ReturnType<typeof withExamples> {
return withExamples(command, invoked(examples));
}

const ROOT_EXAMPLES: HelpExample[] = [
{ description: "Log in once, with your browser", command: "ajs-cloud login" },
{
Expand All @@ -65,11 +85,11 @@ const ROOT_EXAMPLES: HelpExample[] = [

function describeHelpFooter(width: number): string {
const pointer =
`Run ${unbreakable("ajs-cloud <command> --help")} for its options and examples, and ` +
`${unbreakable(`ajs-cloud help ${ENVIRONMENT_TOPIC}`)} for the variables read from the environment.`;
`Run ${unbreakable(asInvoked("ajs-cloud <command> --help"))} for its options and examples, and ` +
`${unbreakable(asInvoked(`ajs-cloud help ${ENVIRONMENT_TOPIC}`))} for the variables read from the environment.`;
return [
"",
formatExamples(ROOT_EXAMPLES, width),
invokedExamples(ROOT_EXAMPLES, width),
"",
...wrapText(pointer, width),
].join("\n");
Expand Down Expand Up @@ -98,7 +118,7 @@ function loginCommand(runtime: Runtime): Command {
const options = globals(self);
await (await actions(options)).login(runtime, options);
});
return withExamples(command, [
return withInvokedExamples(command, [
{ description: "Log in to AntelopeJS Cloud", command: "ajs-cloud login" },
{
description: "Log in to another endpoint",
Expand Down Expand Up @@ -165,7 +185,7 @@ function deployCommand(runtime: Runtime): Command {
).deploy(runtime, options, service, deployOptions);
},
);
return withExamples(command, [
return withInvokedExamples(command, [
{
description: "Deploy every service of the linked folder",
command: "ajs-cloud deploy",
Expand Down Expand Up @@ -206,7 +226,7 @@ function linkCommand(runtime: Runtime): Command {
).link(runtime, options, project, linkOptions);
},
);
return withExamples(command, [
return withInvokedExamples(command, [
{
description: "Link a monorepo with two services",
command:
Expand Down Expand Up @@ -261,7 +281,7 @@ function logsCommand(runtime: Runtime): Command {
).logs(runtime, options, service, logsOptions);
},
);
return withExamples(command, [
return withInvokedExamples(command, [
{ description: "The last hour of logs", command: "ajs-cloud logs web" },
{ description: "Follow new lines", command: "ajs-cloud logs web -f" },
]);
Expand Down Expand Up @@ -291,7 +311,7 @@ function rollbackCommand(runtime: Runtime): Command {
).rollback(runtime, options, service, revision);
},
);
return withExamples(command, [
return withInvokedExamples(command, [
{
description: "Back to the previous revision",
command: "ajs-cloud rollback web",
Expand Down
Loading
Loading