diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 5463cb9..d785add 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -1,4 +1,4 @@
-name: Test Deno Module
+name: Apex tests
on:
push:
@@ -11,8 +11,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: extractions/setup-just@v1
- uses: actions/checkout@v2
- uses: denolib/setup-deno@v2
- name: Run tests
- run: just test
+ run: ./apex test
diff --git a/README.md b/README.md
index 6be206d..caaeb5e 100644
--- a/README.md
+++ b/README.md
@@ -1,32 +1,43 @@
-# Apex CLI
+# Apexlang CLI
-Apex is an interface definition language (IDL) for modeling software. Generate
-source code, documentation, integration, everything automatically.
+The `apex` CLI is a one-stop shop for all projects across all languages.
-### Goals:
+It's a
-- Approachable - Apex was designed from the ground up to be succinct.
- Interfaces and data types are described using familiar syntax that won't slow
- you down.
-- Protocol agnostic - Regardless of the architecture, your data and
- interfaces are fundamentally the same. Use Apex to generate code for any
- serialization format or protocol.
-- Extensible - Generators are written in TypeScript. Easily add
- custom generators that satisfy your unique needs and publish them for everyone
- to use.
+- Project templating and scaffolding tool
+- Extensible code generation tool
+- Task runner
For more information, visit [https://apexlang.io](https://apexlang.io).
-## Installation
+## Prerequisites
+
+The `apex` CLI depends on Deno.
-First, install [Deno](https://github.com/denoland/deno_install).
+Install `deno` with instructions
+[here](https://github.com/denoland/deno_install).
-Then run the command below from a terminal.
+## Installation
+
+To install a release version of the `apex` CLI, run the command below:
```
deno install -A --unstable -f -n apex https://deno.land/x/apex_cli/apex.ts
```
+To install from source, clone this repository and run `./apex run install`
+
+```sh
+git clone https://github.com/apexlang/apex.git
+cd apex
+./apex install # or deno install -A --unstable ./apex.ts
+```
+
+## Usage
+
+Visit [https://apexlang.io](https://apexlang.io) for official documentation and
+usage.
+
```shell
apex --help
```
@@ -39,7 +50,7 @@ Output:
Description:
- A code generation tool using Apex, an interface definition language (IDL) for modeling software.
+ A code generation tool using Apexlang, an interface definition language (IDL) for modeling software.
Options:
@@ -51,16 +62,33 @@ Output:
install - Install templates locally.
new - Create a new project directory using a template.
init - Initialize a project using a template.
- generate [configuration...] - Run apex generators from a given configuration.
+ generate [configuration...] - Run Apexlang generators from a given configuration.
list - List available resources.
describe - Describe available resources.
- watch [configuration...] - Watch apex configuration for changes and trigger code generation.
+ watch [configuration...] - Watch configuration for changes and trigger code generation.
run [tasks...] - Run tasks.
upgrade - Upgrade apex executable to latest or given version.
help [command] - Show this help or the help of a sub-command.
completions - Generate shell completions.
```
+## Running tests
+
+To run tests, run the command below:
+
+```sh
+apex test
+```
+
+## Development
+
+To run the development version of the `apex` CLI, use the `apex` script in the
+root of this repository, e.g.
+
+```sh
+./apex help
+```
+
## Contributing
Please read
diff --git a/apex b/apex
new file mode 100755
index 0000000..80defe0
--- /dev/null
+++ b/apex
@@ -0,0 +1,3 @@
+#!/bin/sh
+# deno install mocker
+exec deno run --allow-all --unstable './apex.ts' "$@"
diff --git a/apex.ts b/apex.ts
index 299a145..43b1a4b 100644
--- a/apex.ts
+++ b/apex.ts
@@ -1,14 +1,7 @@
#!/usr/bin/env -S deno run --allow-read --allow-write --allow-env --allow-net --allow-run --unstable
-import {
- Command,
- CompletionsCommand,
- HelpCommand,
-} from "https://deno.land/x/cliffy@v0.25.5/command/mod.ts";
-import {
- GithubProvider,
- UpgradeCommand,
-} from "https://deno.land/x/cliffy@v0.25.5/command/upgrade/mod.ts";
+import { Command, CompletionsCommand, HelpCommand } from "./src/deps/cliffy.ts";
+import { GithubProvider, UpgradeCommand } from "./src/deps/cliffy.ts";
import * as log from "https://deno.land/std@0.171.0/log/mod.ts";
const LEVEL =
@@ -45,7 +38,7 @@ if (
.version(version)
.name("apex")
.description(
- "A code generation tool using Apex, an interface definition language (IDL) for modeling software.",
+ "A complete project tool suite based on Apexlang, an interface definition language (IDL) for modeling software.",
)
.command("install", install.command)
.command("new", newCmd.command)
@@ -78,7 +71,7 @@ if (
if (nonFlagArgs.length > 0 && !cli.getBaseCommand(args[0], true)) {
const configPath = findApexConfig();
if (!configPath) {
- console.log("could not find configuration");
+ log.error("could not find configuration");
Deno.exit(1);
}
let config;
diff --git a/apex.yaml b/apex.yaml
new file mode 100644
index 0000000..8eb41b8
--- /dev/null
+++ b/apex.yaml
@@ -0,0 +1,10 @@
+tasks:
+ test:
+ description: Run tests
+ cmds:
+ - deno fmt --check src/ test/
+ - deno test --unstable -A
+ install:
+ description: Install apex
+ cmds:
+ - deno install -f -A --unstable ./apex.ts
diff --git a/justfile b/justfile
deleted file mode 100644
index 5b20dba..0000000
--- a/justfile
+++ /dev/null
@@ -1,7 +0,0 @@
-test:
- deno fmt --check src/ test/
- deno test --unstable -A test/*.test.ts
-
-install:
- deno install -f -A --unstable ./apex.ts
-
diff --git a/src/commands/describe.ts b/src/commands/describe.ts
index a77edce..f358190 100644
--- a/src/commands/describe.ts
+++ b/src/commands/describe.ts
@@ -1,5 +1,5 @@
-import { Command } from "https://deno.land/x/cliffy@v0.25.5/command/mod.ts";
-import { Row, Table } from "https://deno.land/x/cliffy@v0.25.5/table/mod.ts";
+import { Command } from "../deps/cliffy.ts";
+import * as ui from "../ui.ts";
import { loadTemplateRegistry } from "../utils.ts";
import { templateCompletion } from "./utils.ts";
@@ -11,7 +11,7 @@ export const templates = new Command()
.action(async (_options, template: string) => {
const registry = await loadTemplateRegistry();
const temp = registry.templates[template];
- if (!template) {
+ if (!temp) {
throw new Error(`template ${template} is not installed`);
}
@@ -23,14 +23,7 @@ export const templates = new Command()
const variables = temp.variables || [];
if (variables.length > 0) {
console.log("\nVariables:");
- new Table()
- .header(Row.from(["Name", "Description", "Default"]).border(true))
- .body(
- variables.map((
- t,
- ) => [t.name, t.description || "", (t.default || "").toString()]),
- )
- .render();
+ ui.listToTable(variables, ["description", "default"]);
}
});
diff --git a/src/commands/generate.ts b/src/commands/generate.ts
index 5322aa0..11cf307 100644
--- a/src/commands/generate.ts
+++ b/src/commands/generate.ts
@@ -1,4 +1,4 @@
-import { Command } from "https://deno.land/x/cliffy@v0.25.5/command/mod.ts";
+import { Command } from "../deps/cliffy.ts";
import * as streams from "https://deno.land/std@0.171.0/streams/read_all.ts";
import * as log from "https://deno.land/std@0.171.0/log/mod.ts";
@@ -7,7 +7,7 @@ import { processConfiguration, writeOutput } from "../process.ts";
export const command = new Command()
.arguments("[...configuration:string[]]")
- .description("Run apex generators from a given configuration.")
+ .description("Run Apexlang generators from a given configuration.")
.action(async (_options: unknown, configFiles: string[]) => {
configFiles ||= [];
if (!configFiles.length) {
diff --git a/src/commands/init.ts b/src/commands/init.ts
index 590a4c6..cc49dcf 100644
--- a/src/commands/init.ts
+++ b/src/commands/init.ts
@@ -1,4 +1,4 @@
-import { Command } from "https://deno.land/x/cliffy@v0.25.5/command/mod.ts";
+import { Command } from "../deps/cliffy.ts";
import { Variables } from "../config.ts";
import {
diff --git a/src/commands/install.ts b/src/commands/install.ts
index 0eba612..291a887 100644
--- a/src/commands/install.ts
+++ b/src/commands/install.ts
@@ -1,7 +1,7 @@
import * as path from "https://deno.land/std@0.171.0/path/mod.ts";
import * as yaml from "https://deno.land/std@0.171.0/encoding/yaml.ts";
-import { Command } from "https://deno.land/x/cliffy@v0.25.5/command/mod.ts";
+import { Command } from "../deps/cliffy.ts";
import { getInstallDirectories } from "../utils.ts";
import { installTemplate } from "../install.ts";
import { TemplateMap, TemplateRegistry } from "../config.ts";
diff --git a/src/commands/list.ts b/src/commands/list.ts
index 09b4312..b5824d8 100644
--- a/src/commands/list.ts
+++ b/src/commands/list.ts
@@ -1,19 +1,29 @@
-import { Command } from "https://deno.land/x/cliffy@v0.25.5/command/mod.ts";
-import { Row, Table } from "https://deno.land/x/cliffy@v0.25.5/table/mod.ts";
+import { Command } from "../deps/cliffy.ts";
+import { action as runAction } from "./run.ts";
import { templateList } from "../utils.ts";
+import * as ui from "../ui.ts";
export const templates = new Command()
.description("List installed templates.")
.action(async (_options) => {
const templates = await templateList();
- new Table()
- .header(Row.from(["Name", "Description"]).border(true))
- .body(templates.map((t) => [t.name, t.description]))
- .render();
+ ui.objToTable(templates, ["version", "description"]);
+ });
+
+export const tasks = new Command()
+ .description("List tasks.")
+ .option(
+ "-c, --config ",
+ "specify an Apex configuration",
+ { default: "apex.yaml" },
+ )
+ .action(async (options) => {
+ await runAction({ list: true, config: options.config }, "");
});
export const command = new Command()
.description("List available resources.")
.default("help")
- .command("templates", templates);
+ .command("templates", templates)
+ .command("tasks", tasks);
diff --git a/src/commands/new.ts b/src/commands/new.ts
index 5836c58..2b3d5ff 100644
--- a/src/commands/new.ts
+++ b/src/commands/new.ts
@@ -1,4 +1,4 @@
-import { Command } from "https://deno.land/x/cliffy@v0.25.5/command/mod.ts";
+import { Command } from "../deps/cliffy.ts";
import { Variables } from "../config.ts";
import {
diff --git a/src/commands/run.ts b/src/commands/run.ts
index ae35017..8b2f114 100644
--- a/src/commands/run.ts
+++ b/src/commands/run.ts
@@ -1,20 +1,22 @@
-import { Command } from "https://deno.land/x/cliffy@v0.25.5/command/mod.ts";
+import { Command } from "../deps/cliffy.ts";
import * as log from "https://deno.land/std@0.171.0/log/mod.ts";
import { fromConfigs } from "./generate.ts";
+import * as ui from "../ui.ts";
-import { Configuration, parseConfigYaml } from "../config.ts";
+import { Configuration, findConfigFile, parseConfigYaml } from "../config.ts";
import { processPlugins } from "../process.ts";
-import { findApexConfig, flatten } from "../utils.ts";
+import { flatten } from "../utils.ts";
import { CmdOutput, Task } from "../task.ts";
export interface RunOptions {
config?: string;
quiet?: boolean;
failUndefined?: boolean;
+ list?: boolean;
}
export const command = new Command()
- .arguments("[...tasks:string[]]")
+ .arguments("[...tasks]")
.option(
"-c, --config ",
"specify an Apex configuration",
@@ -24,38 +26,38 @@ export const command = new Command()
"-q, --quiet",
"silence extraneous apex output",
)
+ .option(
+ "-l, --list",
+ "list tasks",
+ )
.option(
"--fail-undefined",
"when there are multiple configurations, force the command to fail if a task is not defined",
)
.description("Run tasks.")
- .action(async (options: RunOptions, tasks: string[]) => {
- const configFile = options.config || "apex.yaml";
- const configPath = findApexConfig(configFile);
- if (!configPath) {
- console.log("could not find configuration");
- Deno.exit(1);
- }
- let config;
- try {
- config = await Deno.readTextFile(configPath);
- } catch (_e) {
- log.error(`Could not read config ${configPath}`);
- Deno.exit(1);
- }
-
- const configs = parseConfigYaml(config);
- for (const cfg of configs) {
- const taskMap = await loadTasks(cfg);
- await runTasks(
- cfg,
- taskMap,
- tasks,
- configs.length == 1 || options.failUndefined == true,
- options,
- );
+ .action(action);
+
+export async function action(
+ options: RunOptions,
+ ...tasks: string[]
+) {
+ const config = await findConfigFile(options.config);
+ const configs = parseConfigYaml(config);
+ for (const cfg of configs) {
+ const taskMap = await loadTasks(cfg);
+ if (options.list) {
+ ui.objToTable(taskMap, ["description"]);
+ continue;
}
- });
+ await runTasks(
+ cfg,
+ taskMap,
+ tasks,
+ configs.length == 1 || options.failUndefined == true,
+ options,
+ );
+ }
+}
export function parseTasks(
config: Configuration,
@@ -98,7 +100,6 @@ export async function loadTasks(
config: Configuration,
): Promise> {
config = await processPlugins(config);
-
return parseTasks(config);
}
diff --git a/src/commands/utils.ts b/src/commands/utils.ts
index e85009d..68484de 100644
--- a/src/commands/utils.ts
+++ b/src/commands/utils.ts
@@ -1,4 +1,4 @@
-import { ValidationError } from "https://deno.land/x/cliffy@v0.25.5/command/mod.ts";
+import { ValidationError } from "../deps/cliffy.ts";
import { templateList } from "../utils.ts";
export const varOptions = {
@@ -23,5 +23,5 @@ export const varOptions = {
};
export async function templateCompletion(): Promise {
- return (await templateList()).map((t) => t.name || "");
+ return Object.values(await templateList()).map((t) => t.name || "");
}
diff --git a/src/commands/watch.ts b/src/commands/watch.ts
index a4038c4..4ae1173 100644
--- a/src/commands/watch.ts
+++ b/src/commands/watch.ts
@@ -1,4 +1,4 @@
-import { Command } from "https://deno.land/x/cliffy@v0.25.5/command/mod.ts";
+import { Command } from "../deps/cliffy.ts";
import * as yaml from "https://deno.land/std@0.171.0/encoding/yaml.ts";
import * as log from "https://deno.land/std@0.171.0/log/mod.ts";
import * as path from "https://deno.land/std@0.171.0/path/mod.ts";
@@ -9,7 +9,7 @@ import { processConfiguration, writeOutput } from "../process.ts";
export const command = new Command()
.arguments("[...configuration:string[]]")
.description(
- "Watch apex configuration for changes and trigger code generation.",
+ "Watch configuration for changes and trigger code generation.",
)
.action(async (_options: unknown, configFiles: string[]) => {
configFiles = configFiles || [];
diff --git a/src/config.ts b/src/config.ts
index b258417..5edfc32 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,5 +1,7 @@
import { TaskConfig } from "./task.ts";
import * as yaml from "https://deno.land/std@0.171.0/encoding/yaml.ts";
+import { findApexConfig } from "./utils.ts";
+import { log } from "./deps/log.ts";
export type Config = { [key: string]: unknown };
@@ -57,6 +59,7 @@ export type TemplateMap = Record;
export interface InstalledTemplate extends TemplateInfo {
url: string;
+ version?: string;
}
export interface TemplateRegistry {
@@ -117,5 +120,22 @@ export function parseConfigYaml(contents: string): Configuration[] {
return contents
.split("---\n")
.map((v) => v.trim())
- .map((v) => yaml.parse(v) as Configuration);
+ .map((v) => (yaml.parse(v) || {}) as Configuration);
+}
+
+export async function findConfigFile(filePath?: string): Promise {
+ const configFile = filePath || "apex.yaml";
+ const configPath = findApexConfig(configFile);
+ if (!configPath) {
+ console.log("could not find configuration");
+ Deno.exit(1);
+ }
+ let config;
+ try {
+ config = await Deno.readTextFile(configPath);
+ } catch (_e) {
+ log.error(`Could not read config ${configPath}`);
+ Deno.exit(1);
+ }
+ return config;
}
diff --git a/src/deps/cliffy.ts b/src/deps/cliffy.ts
new file mode 100644
index 0000000..cfe6db6
--- /dev/null
+++ b/src/deps/cliffy.ts
@@ -0,0 +1 @@
+export * from "https://deno.land/x/cliffy@v0.25.7/mod.ts";
diff --git a/src/deps/log.ts b/src/deps/log.ts
new file mode 100644
index 0000000..b0132a0
--- /dev/null
+++ b/src/deps/log.ts
@@ -0,0 +1 @@
+export * as log from "https://deno.land/std@0.171.0/log/mod.ts";
diff --git a/src/init.ts b/src/init.ts
index 07de565..4f75cdc 100644
--- a/src/init.ts
+++ b/src/init.ts
@@ -9,7 +9,7 @@ import {
InputOptions,
Number,
NumberOptions,
-} from "https://deno.land/x/cliffy@v0.25.5/prompt/mod.ts";
+} from "./deps/cliffy.ts";
import * as eta from "https://deno.land/x/eta@v1.12.3/mod.ts";
import {
diff --git a/src/task.ts b/src/task.ts
index 74656a5..4987d22 100644
--- a/src/task.ts
+++ b/src/task.ts
@@ -13,6 +13,7 @@ export interface CmdOutput {
export class Task {
runner = TaskRunner.Dax;
+ description?: string;
deps: string[] = [];
cmds: string[] = [];
@@ -20,6 +21,7 @@ export class Task {
this.runner = config.runner || TaskRunner.Dax;
this.deps = config.deps || [];
this.cmds = config.cmds || [];
+ this.description = config.description || "";
}
async run(
diff --git a/src/ui.ts b/src/ui.ts
new file mode 100644
index 0000000..8d95a55
--- /dev/null
+++ b/src/ui.ts
@@ -0,0 +1,51 @@
+import { Cell, colors, Row, Table } from "./deps/cliffy.ts";
+
+export function double(value?: unknown): Cell {
+ return new Cell((value || "").toString()).colSpan(2);
+}
+
+export function primary(value: string): string {
+ return colors.bold.green(value);
+}
+
+const DOUBLE_SIZE_CELLS: (string | number | symbol)[] = ["description"];
+
+function isDoubleSize(name: string | number | symbol): boolean {
+ return DOUBLE_SIZE_CELLS.includes(name);
+}
+
+function cap(name: string | number | symbol): string {
+ const stringed = name.toString();
+ return stringed[0].toUpperCase() + stringed.slice(1);
+}
+
+export function objToTable(obj: Record, columns: (keyof T)[]) {
+ const table = new Table().minColWidth(20);
+ const header: (string | Cell)[] = ["Name"];
+
+ header.push(...columns.map((c) => isDoubleSize(c) ? double(cap(c)) : cap(c)));
+ table.header(Row.from(header).border(true));
+ if (Object.keys(obj).length === 0) {
+ table.body([Row.from([double("No results.")])]);
+ } else {
+ for (const name in obj) {
+ // deno-lint-ignore no-explicit-any
+ const row: (any | Cell)[] = [new Cell(primary(name))];
+ for (const col of columns) {
+ row.push(isDoubleSize(col) ? double(obj[name][col]) : obj[name][col]);
+ }
+ table.push(Row.from(row));
+ }
+ }
+ table.sort();
+ table.render();
+}
+
+// This turns an array of objects
+export function listToTable(
+ obj: ({ name: string } & T)[],
+ columns: (keyof T)[],
+) {
+ const record = Object.fromEntries(obj.map((o) => [o["name"], o]));
+ objToTable(record, columns);
+}
diff --git a/src/utils.ts b/src/utils.ts
index 283ee3a..eaa88c8 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -24,7 +24,9 @@ export async function loadTemplateRegistry(): Promise {
const templateRegistry = path.join(dirs.home, "templates.yaml");
try {
const templateListYAML = Deno.readTextFileSync(templateRegistry);
- return yaml.parse(templateListYAML) as TemplateRegistry;
+ const registry = yaml.parse(templateListYAML) as TemplateRegistry;
+ calulateVersions(registry);
+ return registry;
} catch (error) {
if (error instanceof Deno.errors.NotFound) {
return {
@@ -35,10 +37,31 @@ export async function loadTemplateRegistry(): Promise {
}
}
-export async function templateList(): Promise {
+const versionRegex = /@(v[0-9][^\/]*)\//gm;
+
+function calulateVersions(registry: TemplateRegistry) {
+ for (const tmpl of Object.values(registry.templates)) {
+ if (tmpl.version) {
+ continue;
+ }
+
+ // Get version
+ let m;
+ if ((m = versionRegex.exec(tmpl.url)) !== null) {
+ m.forEach((match, groupIndex) => {
+ if (groupIndex == 1) {
+ tmpl.version = match;
+ }
+ });
+ }
+ }
+}
+
+export async function templateList(): Promise<
+ Record
+> {
const allTemplates = await loadTemplateRegistry();
- const templates = Object.values(allTemplates.templates);
- return templates.sort((a, b) => new String(a.name).localeCompare(b.name));
+ return allTemplates.templates;
}
export interface ApexDirs {
@@ -132,13 +155,13 @@ export function findApexConfig(config = "apex.yaml"): string | undefined {
}
}
-export function flatten(prefix: string, obj: any): any {
+export function flatten(prefix: string, obj: unknown): unknown {
if (obj === null || obj === undefined) {
return { [prefix]: "" };
} else if (typeof obj === "string") {
return { [prefix]: obj };
} else if (Array.isArray(obj)) {
- const result: any = {};
+ const result = {};
for (let i = 0; i < obj.length; i++) {
Object.assign(result, flatten(`${prefix}_${i}`, obj[i]));
}