Skip to content

Commit

Permalink
feat(command): add upgrade command (#203)
Browse files Browse the repository at this point in the history
  • Loading branch information
c4spar committed May 19, 2021
1 parent 143eb1b commit 348f743
Show file tree
Hide file tree
Showing 9 changed files with 622 additions and 0 deletions.
102 changes: 102 additions & 0 deletions command/README.md
Expand Up @@ -93,6 +93,7 @@
- [Generic instance method types](#generic-instance-method-types)
- [Generic constructor types](#generic-constructor-types)
- [Generic global parent types](#generic-global-parent-types)
- [Upgrade command](#-upgrade-command)
- [Version option](#-version-option)
- [Contributing](#-contributing)
- [License](#-license)
Expand Down Expand Up @@ -1652,6 +1653,107 @@ await new Command<void>()
});
```

## ❯ Upgrade command

Cliffy provides an `UpgradeCommand` that can be used to upgrade your cli to a
given or latest version.

```shell
COMMAND upgrade --version 1.0.2
```

If you register the `upgrade` command you need to register an registry
`provider`. Optional you can define the `main` file of your cli which defaults
to the name of your cli (`[name].ts`) and `args` which can be used to define
permissions that are passed to `deno install`.

If no `args` are defined, following args are set by default: `--no-check`,
`--quiet`, `--force` and `--name`. `--no-check` and `--quiet` are not set by
default if `args` are defined. `--force` and `--name` are always set by default.

```typescript
import { UpgradeCommand } from "https://deno.land/x/cliffy/command/upgrade/mod.ts";
cmd.command(
"upgrade",
new UpgradeCommand({
main: "cliffy.ts",
args: ["--allow-net", "--unstable"],
provider: new DenoLandProvider(),
}),
);
```

There are three build in providers: `deno.land`, `nest.land` and `github`. If
multiple providers are registered, you can specify the registry that should be
used with the `--registry` option. The github provider can also be used to
`upgrade` to any git branch.

```shell
COMMAND upgrade --registry github --version main
```

The `--registry` option is hidden if only one provider is registerd. If the
`upgrade` command is called without the `--registry` option, the default
registry is used. The default registry is the first registered provider.

The `GithubProvider` requires the `repository` name as option. The
`DenoLandProvider` and `NestLandProvider` does not require any options but you
can optionally pass the registry module name to the provider which defaults to
the command name.

```typescript
cmd.command(
"upgrade",
new UpgradeCommand({
provider: [
new DenoLandProvider({ name: "cliffy" }),
new NestLandProvider({ name: "cliffy" }),
new GithubProvider({ repository: "c4spar/deno-cliffy" }),
],
}),
);
```

The upgrade command can be also used, to list all available versions with the
`-l` or `--list-versions` option. The current installed version is highlighted
and prefix with a `*`.

```shell
COMMAND upgrade -l
```

```
* v0.2.2
v0.2.1
v0.2.0
v0.1.0
```

The github registry shows all available tags and branches. Branches can be
disabled with the `branches` option `GithubProvider({ branches: false })`. If
the versions list is larger than `25`, the versions are displayed as table.

```shell
COMMAND upgrade --registry github --list-versions
```

```
Tags:
v0.18.2 v0.17.0 v0.14.1 v0.11.2 v0.8.2 v0.6.1 v0.3.0
v0.18.1 * v0.16.0 v0.14.0 v0.11.1 v0.8.1 v0.6.0 v0.2.0
v0.18.0 v0.15.0 v0.13.0 v0.11.0 v0.8.0 v0.5.1 v0.1.0
v0.17.2 v0.14.3 v0.12.1 v0.10.0 v0.7.1 v0.5.0
v0.17.1 v0.14.2 v0.12.0 v0.9.0 v0.7.0 v0.4.0
Branches:
main (Protected)
keypress/add-keypress-module
keycode/refactoring
command/upgrade-command
```

## ❯ Version option

The `--version` and `-V` option flag prints the version number defined with the
Expand Down
1 change: 1 addition & 0 deletions command/deps.ts
@@ -1,6 +1,7 @@
export {
blue,
bold,
cyan,
dim,
green,
italic,
Expand Down
4 changes: 4 additions & 0 deletions command/upgrade/mod.ts
@@ -0,0 +1,4 @@
export * from "./provider/deno_land.ts";
export * from "./provider/nest_land.ts";
export * from "./provider.ts";
export * from "./upgrade_command.ts";
163 changes: 163 additions & 0 deletions command/upgrade/provider.ts
@@ -0,0 +1,163 @@
import { blue, bold, cyan, green, red, yellow } from "../deps.ts";
import { ValidationError } from "../_errors.ts";
import { Table } from "../../table/table.ts";

export interface Versions {
latest: string;
versions: Array<string>;
}

export interface UpgradeOptions {
name: string;
from?: string;
to: string;
args?: Array<string>;
main?: string;
}

export abstract class Provider {
abstract readonly name: string;
protected readonly maxListSize: number = 25;
private maxCols = 8;

abstract getVersions(name: string): Promise<Versions>;

abstract getRepositoryUrl(name: string): string;

abstract getRegistryUrl(name: string, version: string): string;

async isOutdated(
name: string,
currentVersion: string,
targetVersion: string,
): Promise<boolean> {
const { latest, versions } = await this.getVersions(name);

if (targetVersion === "latest") {
targetVersion = latest;
}

// Check if requested version exists.
if (targetVersion && !versions.includes(targetVersion)) {
throw new ValidationError(
`The provided version ${
bold(red(targetVersion))
} is not found.\n\n ${
cyan(
`Visit ${
blue(this.getRepositoryUrl(name))
} for available releases or run again with the ${(yellow(
"-l",
))} or ${(yellow("--list-versions"))} command.`,
)
}`,
);
}

// Check if requested version is already the latest available version.
if (latest && latest === currentVersion && latest === targetVersion) {
console.warn(
yellow(
`You're already using the latest available version ${currentVersion} of ${name}.`,
),
);
return false;
}

// Check if requested version is already installed.
if (targetVersion && currentVersion === targetVersion) {
console.warn(
yellow(`You're already using version ${currentVersion} of ${name}.`),
);
return false;
}

return true;
}

async upgrade(
{ name, from, to, main = `${name}.ts`, args = [] }: UpgradeOptions,
): Promise<void> {
if (to === "latest") {
const { latest } = await this.getVersions(name);
to = latest;
}
const registry: string = new URL(main, this.getRegistryUrl(name, to)).href;

const cmd = [Deno.execPath(), "install"];
if (args.length) {
cmd.push(...args, "--force", "--name", name, registry);
} else {
cmd.push("--no-check", "--quiet", "--force", "--name", name, registry);
}

const process = Deno.run({ cmd, stdout: "piped", stderr: "piped" });

const [status, stderr] = await Promise.all([
process.status(),
process.stderrOutput(),
process.output(),
]);

if (!status.success) {
process.close();
await Deno.stderr.write(stderr);
throw new Error(
`Failed to upgrade ${name} from ${from} to version ${to}!`,
);
}
process.close();

console.info(
`Successfully upgraded ${name} from ${from} to version ${to}! (${
this.getRegistryUrl(name, to)
})`,
);
}

public async listVersions(
name: string,
currentVersion?: string,
): Promise<void> {
const { versions } = await this.getVersions(name);
this.printVersions(versions, currentVersion);
}

protected printVersions(
versions: Array<string>,
currentVersion?: string,
{ maxCols = this.maxCols, indent = 0 }: {
maxCols?: number;
indent?: number;
} = {},
): void {
versions = versions.slice();
if (versions?.length) {
versions = versions.map((version: string) =>
currentVersion && currentVersion === version
? green(`* ${version}`)
: ` ${version}`
);

if (versions.length > this.maxListSize) {
const table = new Table().indent(indent);
const rowSize = Math.ceil(versions.length / maxCols);
const colSize = Math.min(versions.length, maxCols);
let versionIndex = 0;
for (let colIndex = 0; colIndex < colSize; colIndex++) {
for (let rowIndex = 0; rowIndex < rowSize; rowIndex++) {
if (!table[rowIndex]) {
table[rowIndex] = [];
}
table[rowIndex][colIndex] = versions[versionIndex++];
}
}
console.log(table.toString());
} else {
console.log(
versions.map((version) => " ".repeat(indent) + version).join("\n"),
);
}
}
}
}
41 changes: 41 additions & 0 deletions command/upgrade/provider/deno_land.ts
@@ -0,0 +1,41 @@
import { Provider, Versions } from "../provider.ts";

export interface DenoLandProviderOptions {
name?: string;
}

export class DenoLandProvider extends Provider {
name = "deno.land";
private readonly repositoryUrl = "https://deno.land/x/";
private readonly registryUrl = "https://deno.land/x/";
private readonly moduleName?: string;

constructor({ name }: DenoLandProviderOptions = {}) {
super();
this.moduleName = name;
}

async getVersions(
name: string,
): Promise<Versions> {
const response = await fetch(
`https://cdn.deno.land/${this.moduleName ?? name}/meta/versions.json`,
);
if (!response.ok) {
throw new Error(
"couldn't fetch the latest version - try again after sometime",
);
}

return await response.json();
}

getRepositoryUrl(name: string): string {
return new URL(`${this.moduleName ?? name}/`, this.repositoryUrl).href;
}

getRegistryUrl(name: string, version: string): string {
return new URL(`${this.moduleName ?? name}@${version}/`, this.registryUrl)
.href;
}
}

0 comments on commit 348f743

Please sign in to comment.