@velajs/cli 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 +57 -0
- package/dist/commands/introspect.commands.d.ts +40 -0
- package/dist/commands/introspect.commands.js +165 -0
- package/dist/commands/seed.command.d.ts +9 -0
- package/dist/commands/seed.command.js +40 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.js +42 -0
- package/dist/format.d.ts +8 -0
- package/dist/format.js +32 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +23 -0
- package/dist/introspect.d.ts +32 -0
- package/dist/introspect.js +103 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @velajs/cli
|
|
2
|
+
|
|
3
|
+
Node-side command-line tools for [Vela](https://github.com/velajs/vela) apps. Runs
|
|
4
|
+
outside the edge Worker (it uses `node:*` / `process`), so it stays out of your
|
|
5
|
+
Worker bundle.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add -D @velajs/cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Commands
|
|
14
|
+
|
|
15
|
+
| Command | What it does |
|
|
16
|
+
| --- | --- |
|
|
17
|
+
| `vela db seed` | Build the app and run all `@Seeder()` classes in order. |
|
|
18
|
+
| `vela route list` | HTTP route table: framework-composed controller routes (`Controller#handler`, full paths incl. prefix/version) plus `(mounted)` extras (CRUD/contributed, doc UIs). |
|
|
19
|
+
| `vela module graph` | Module graph: imports tree with `global`/`lazy` flags and provider counts (`--json` for the raw graph). |
|
|
20
|
+
| `vela entrypoint list` | Declared entrypoint kinds (websocket, queue, cron, …) and their entries — lazy modules stay unmaterialized. |
|
|
21
|
+
| `vela openapi dump` | Emit the OpenAPI document (needs `rootModule` in the config; `--out`, `--title`, `--api-version`, `--global-prefix`). |
|
|
22
|
+
|
|
23
|
+
All introspection commands take `--config <path>` and `--json`.
|
|
24
|
+
|
|
25
|
+
## Configure
|
|
26
|
+
|
|
27
|
+
Create a `vela.config.{js,mjs,ts}` at your project root that builds your app.
|
|
28
|
+
Wire your runtime bindings here (e.g. via miniflare for Cloudflare, or a Node
|
|
29
|
+
adapter):
|
|
30
|
+
|
|
31
|
+
```ts
|
|
32
|
+
// vela.config.ts
|
|
33
|
+
import { defineVelaConfig } from '@velajs/cli/config';
|
|
34
|
+
import { AppModule } from './src/app.module';
|
|
35
|
+
|
|
36
|
+
export default defineVelaConfig({
|
|
37
|
+
rootModule: AppModule, // optional — needed by `vela openapi dump`
|
|
38
|
+
async createApp() {
|
|
39
|
+
const { createCloudflareApp } = await import('@velajs/cloudflare');
|
|
40
|
+
return createCloudflareApp(AppModule);
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
> `.ts` configs require a runtime that strips types (Node 22+
|
|
46
|
+
> `--experimental-strip-types`, or `tsx`). `.js`/`.mjs` load directly.
|
|
47
|
+
|
|
48
|
+
## Commands
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
# Run all @Seeder() classes (see @velajs/vela/seeder), in order:
|
|
52
|
+
vela db seed
|
|
53
|
+
vela db seed --config ./config/vela.config.js
|
|
54
|
+
vela db seed --continue-on-error
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Exit code is `0` when all seeders run and `1` if any fail.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { VelaApplication } from '@velajs/vela';
|
|
2
|
+
import { Command } from 'clipanion';
|
|
3
|
+
/** Shared shell: load config → createApp → run → best-effort dispose. */
|
|
4
|
+
declare abstract class AppCommand extends Command {
|
|
5
|
+
config: string | undefined;
|
|
6
|
+
json: boolean;
|
|
7
|
+
protected abstract run(app: VelaApplication): Promise<number>;
|
|
8
|
+
execute(): Promise<number>;
|
|
9
|
+
protected print(text: string): void;
|
|
10
|
+
}
|
|
11
|
+
/** `vela route list` — the app's HTTP route table. */
|
|
12
|
+
export declare class RouteListCommand extends AppCommand {
|
|
13
|
+
static paths: string[][];
|
|
14
|
+
static usage: import("clipanion").Usage;
|
|
15
|
+
protected run(app: VelaApplication): Promise<number>;
|
|
16
|
+
}
|
|
17
|
+
/** `vela module graph` — the loaded module graph. */
|
|
18
|
+
export declare class ModuleGraphCommand extends AppCommand {
|
|
19
|
+
static paths: string[][];
|
|
20
|
+
static usage: import("clipanion").Usage;
|
|
21
|
+
protected run(app: VelaApplication): Promise<number>;
|
|
22
|
+
}
|
|
23
|
+
/** `vela entrypoint list` — declared entrypoint kinds and their entries. */
|
|
24
|
+
export declare class EntrypointListCommand extends AppCommand {
|
|
25
|
+
static paths: string[][];
|
|
26
|
+
static usage: import("clipanion").Usage;
|
|
27
|
+
protected run(app: VelaApplication): Promise<number>;
|
|
28
|
+
}
|
|
29
|
+
/** `vela openapi dump` — emit the OpenAPI document. */
|
|
30
|
+
export declare class OpenApiDumpCommand extends Command {
|
|
31
|
+
static paths: string[][];
|
|
32
|
+
static usage: import("clipanion").Usage;
|
|
33
|
+
config: string | undefined;
|
|
34
|
+
out: string | undefined;
|
|
35
|
+
title: string | undefined;
|
|
36
|
+
apiVersion: string | undefined;
|
|
37
|
+
globalPrefix: string | undefined;
|
|
38
|
+
execute(): Promise<number>;
|
|
39
|
+
}
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createOpenApiDocument } from '@velajs/vela';
|
|
3
|
+
import { Command, Option } from 'clipanion';
|
|
4
|
+
import { loadConfig } from '../config.js';
|
|
5
|
+
import { renderTable } from '../format.js';
|
|
6
|
+
import { collectEntrypoints, collectModules, collectRoutes, renderModuleTree } from '../introspect.js';
|
|
7
|
+
/** Shared shell: load config → createApp → run → best-effort dispose. */
|
|
8
|
+
class AppCommand extends Command {
|
|
9
|
+
config = Option.String('--config', { description: 'Path to the vela config file.' });
|
|
10
|
+
json = Option.Boolean('--json', false, { description: 'Emit machine-readable JSON.' });
|
|
11
|
+
async execute() {
|
|
12
|
+
const velaConfig = await loadConfig(process.cwd(), this.config);
|
|
13
|
+
const app = await velaConfig.createApp();
|
|
14
|
+
try {
|
|
15
|
+
return await this.run(app);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
const dispose = app.dispose;
|
|
19
|
+
if (typeof dispose === 'function') {
|
|
20
|
+
try {
|
|
21
|
+
await dispose.call(app);
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
this.context.stderr.write(`Warning: teardown failed: ${String(error)}\n`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
print(text) {
|
|
30
|
+
this.context.stdout.write(`${text}\n`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** `vela route list` — the app's HTTP route table. */
|
|
34
|
+
export class RouteListCommand extends AppCommand {
|
|
35
|
+
static paths = [['route', 'list']];
|
|
36
|
+
static usage = Command.Usage({
|
|
37
|
+
category: 'Introspection',
|
|
38
|
+
description: 'List the HTTP routes of the Vela app.',
|
|
39
|
+
details: 'Framework-composed controller routes (method, full path, controller#handler) plus ' +
|
|
40
|
+
'everything else mounted on the router (CRUD/contributed routes, doc UIs) labeled (mounted).',
|
|
41
|
+
examples: [['List routes', 'vela route list'], ['As JSON', 'vela route list --json']],
|
|
42
|
+
});
|
|
43
|
+
async run(app) {
|
|
44
|
+
const rows = collectRoutes(app);
|
|
45
|
+
if (rows === null) {
|
|
46
|
+
this.print('This app builds no HTTP routes — nothing to list.');
|
|
47
|
+
return 0;
|
|
48
|
+
}
|
|
49
|
+
if (this.json) {
|
|
50
|
+
this.print(JSON.stringify(rows, null, 2));
|
|
51
|
+
return 0;
|
|
52
|
+
}
|
|
53
|
+
for (const line of renderTable(['METHOD', 'PATH', 'HANDLER'], rows.map((r) => [r.method, r.path, r.handler]))) {
|
|
54
|
+
this.print(line);
|
|
55
|
+
}
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** `vela module graph` — the loaded module graph. */
|
|
60
|
+
export class ModuleGraphCommand extends AppCommand {
|
|
61
|
+
static paths = [['module', 'graph']];
|
|
62
|
+
static usage = Command.Usage({
|
|
63
|
+
category: 'Introspection',
|
|
64
|
+
description: 'Print the module graph of the Vela app.',
|
|
65
|
+
details: 'Module instances with their imports (indented tree), global/lazy flags, and provider ' +
|
|
66
|
+
'counts. --json emits the raw descriptions (providers, exports, imports per module).',
|
|
67
|
+
examples: [['Print the graph', 'vela module graph'], ['As JSON', 'vela module graph --json']],
|
|
68
|
+
});
|
|
69
|
+
async run(app) {
|
|
70
|
+
const modules = collectModules(app);
|
|
71
|
+
if (this.json) {
|
|
72
|
+
this.print(JSON.stringify(modules, null, 2));
|
|
73
|
+
return 0;
|
|
74
|
+
}
|
|
75
|
+
for (const line of renderModuleTree(modules))
|
|
76
|
+
this.print(line);
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** `vela entrypoint list` — declared entrypoint kinds and their entries. */
|
|
81
|
+
export class EntrypointListCommand extends AppCommand {
|
|
82
|
+
static paths = [['entrypoint', 'list']];
|
|
83
|
+
static usage = Command.Usage({
|
|
84
|
+
category: 'Introspection',
|
|
85
|
+
description: 'List entrypoint kinds and entries (websocket, queue, cron, …).',
|
|
86
|
+
details: 'Every declared kind — including kinds with zero entries — with the contributing ' +
|
|
87
|
+
'class (and method for method-level kinds) and its metadata.',
|
|
88
|
+
examples: [['List entrypoints', 'vela entrypoint list']],
|
|
89
|
+
});
|
|
90
|
+
async run(app) {
|
|
91
|
+
const rows = collectEntrypoints(app);
|
|
92
|
+
if (this.json) {
|
|
93
|
+
this.print(JSON.stringify(rows, null, 2));
|
|
94
|
+
return 0;
|
|
95
|
+
}
|
|
96
|
+
for (const line of renderTable(['KIND', 'TARGET', 'META'], rows.map((r) => [r.kind, r.target, r.meta]))) {
|
|
97
|
+
this.print(line);
|
|
98
|
+
}
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** `vela openapi dump` — emit the OpenAPI document. */
|
|
103
|
+
export class OpenApiDumpCommand extends Command {
|
|
104
|
+
static paths = [['openapi', 'dump']];
|
|
105
|
+
static usage = Command.Usage({
|
|
106
|
+
category: 'Introspection',
|
|
107
|
+
description: 'Emit the OpenAPI document for the Vela app.',
|
|
108
|
+
details: "Requires `rootModule` in vela.config (createOpenApiDocument works from the module " +
|
|
109
|
+
"class). The app's global prefix is applied automatically; --global-prefix overrides.",
|
|
110
|
+
examples: [
|
|
111
|
+
['Print to stdout', 'vela openapi dump'],
|
|
112
|
+
['Write to a file', 'vela openapi dump --out openapi.json'],
|
|
113
|
+
],
|
|
114
|
+
});
|
|
115
|
+
config = Option.String('--config', { description: 'Path to the vela config file.' });
|
|
116
|
+
out = Option.String('--out', { description: 'Write the document to this file instead of stdout.' });
|
|
117
|
+
title = Option.String('--title', { description: 'info.title override.' });
|
|
118
|
+
apiVersion = Option.String('--api-version', { description: 'info.version override.' });
|
|
119
|
+
globalPrefix = Option.String('--global-prefix', {
|
|
120
|
+
description: "Path prefix override (defaults to the app's global prefix).",
|
|
121
|
+
});
|
|
122
|
+
async execute() {
|
|
123
|
+
const velaConfig = await loadConfig(process.cwd(), this.config);
|
|
124
|
+
if (!velaConfig.rootModule) {
|
|
125
|
+
this.context.stderr.write("openapi dump needs the root module. Add it to your vela.config:\n\n" +
|
|
126
|
+
' export default defineVelaConfig({\n' +
|
|
127
|
+
' rootModule: AppModule,\n' +
|
|
128
|
+
' async createApp() { ... },\n' +
|
|
129
|
+
' });\n');
|
|
130
|
+
return 1;
|
|
131
|
+
}
|
|
132
|
+
const app = await velaConfig.createApp();
|
|
133
|
+
try {
|
|
134
|
+
const info = {};
|
|
135
|
+
if (this.title)
|
|
136
|
+
info.title = this.title;
|
|
137
|
+
if (this.apiVersion)
|
|
138
|
+
info.version = this.apiVersion;
|
|
139
|
+
const document = createOpenApiDocument(velaConfig.rootModule, {
|
|
140
|
+
globalPrefix: this.globalPrefix ?? app.getGlobalPrefix(),
|
|
141
|
+
...(Object.keys(info).length > 0 ? { info } : {}),
|
|
142
|
+
});
|
|
143
|
+
const text = JSON.stringify(document, null, 2);
|
|
144
|
+
if (this.out) {
|
|
145
|
+
await writeFile(this.out, `${text}\n`, 'utf8');
|
|
146
|
+
this.context.stdout.write(`Wrote ${this.out}\n`);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
this.context.stdout.write(`${text}\n`);
|
|
150
|
+
}
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
const dispose = app.dispose;
|
|
155
|
+
if (typeof dispose === 'function') {
|
|
156
|
+
try {
|
|
157
|
+
await dispose.call(app);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
this.context.stderr.write(`Warning: teardown failed: ${String(error)}\n`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from 'clipanion';
|
|
2
|
+
/** `vela db seed` — build the app from vela.config and run its seeders. */
|
|
3
|
+
export declare class SeedCommand extends Command {
|
|
4
|
+
static paths: string[][];
|
|
5
|
+
static usage: import("clipanion").Usage;
|
|
6
|
+
config: string | undefined;
|
|
7
|
+
continueOnError: boolean;
|
|
8
|
+
execute(): Promise<number>;
|
|
9
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { runSeeders } from '@velajs/vela/seeder';
|
|
2
|
+
import { Command, Option } from 'clipanion';
|
|
3
|
+
import { loadConfig } from '../config.js';
|
|
4
|
+
import { formatSeedResults } from '../format.js';
|
|
5
|
+
/** `vela db seed` — build the app from vela.config and run its seeders. */
|
|
6
|
+
export class SeedCommand extends Command {
|
|
7
|
+
static paths = [['db', 'seed']];
|
|
8
|
+
static usage = Command.Usage({
|
|
9
|
+
category: 'Database',
|
|
10
|
+
description: 'Run database seeders for the Vela app.',
|
|
11
|
+
details: 'Loads vela.config.{js,mjs,ts}, builds the app, and runs all @Seeder() classes in order.',
|
|
12
|
+
examples: [
|
|
13
|
+
['Run all seeders', 'vela db seed'],
|
|
14
|
+
['Use a specific config', 'vela db seed --config ./config/vela.config.js'],
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
config = Option.String('--config', { description: 'Path to the vela config file.' });
|
|
18
|
+
continueOnError = Option.Boolean('--continue-on-error', false, {
|
|
19
|
+
description: 'Run all seeders even if one fails.',
|
|
20
|
+
});
|
|
21
|
+
async execute() {
|
|
22
|
+
const { createApp } = await loadConfig(process.cwd(), this.config);
|
|
23
|
+
const app = await createApp();
|
|
24
|
+
this.context.stdout.write('Running seeders…\n');
|
|
25
|
+
const results = await runSeeders(app, { stopOnError: !this.continueOnError });
|
|
26
|
+
const code = formatSeedResults(results, (message) => this.context.stdout.write(`${message}\n`));
|
|
27
|
+
// Best-effort teardown (VelaApplication.dispose exists on recent versions).
|
|
28
|
+
// Must not clobber the computed exit code if a shutdown hook throws.
|
|
29
|
+
const dispose = app.dispose;
|
|
30
|
+
if (typeof dispose === 'function') {
|
|
31
|
+
try {
|
|
32
|
+
await dispose.call(app);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
this.context.stderr.write(`Warning: teardown failed after seeding: ${String(error)}\n`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return code;
|
|
39
|
+
}
|
|
40
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Type, VelaApplication } from '@velajs/vela';
|
|
2
|
+
/**
|
|
3
|
+
* A `vela.config.{js,mjs,ts}` default-exports (or exports `config`) this shape.
|
|
4
|
+
* You wire your runtime bindings inside `createApp` — e.g. via miniflare for a
|
|
5
|
+
* Cloudflare Worker, or a plain Node adapter — and return a built app.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* // vela.config.ts
|
|
9
|
+
* import { defineVelaConfig } from '@velajs/cli/config';
|
|
10
|
+
* export default defineVelaConfig({
|
|
11
|
+
* async createApp() {
|
|
12
|
+
* const { createCloudflareApp } = await import('@velajs/cloudflare');
|
|
13
|
+
* return createCloudflareApp(AppModule);
|
|
14
|
+
* },
|
|
15
|
+
* });
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export interface VelaConfig {
|
|
19
|
+
createApp(): Promise<VelaApplication> | VelaApplication;
|
|
20
|
+
/**
|
|
21
|
+
* The app's root module class — needed only by commands that work from
|
|
22
|
+
* module metadata rather than the built app (`vela openapi dump`).
|
|
23
|
+
*/
|
|
24
|
+
rootModule?: Type;
|
|
25
|
+
}
|
|
26
|
+
/** Identity helper for type-safe config files. */
|
|
27
|
+
export declare function defineVelaConfig(config: VelaConfig): VelaConfig;
|
|
28
|
+
/**
|
|
29
|
+
* Locate + import the vela config. `.ts` requires a runtime that strips types
|
|
30
|
+
* (Node 22+ `--experimental-strip-types`, or tsx/ts-node); `.js`/`.mjs` load
|
|
31
|
+
* directly.
|
|
32
|
+
*/
|
|
33
|
+
export declare function loadConfig(cwd?: string, explicitPath?: string): Promise<VelaConfig>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { access } from 'node:fs/promises';
|
|
2
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
/** Identity helper for type-safe config files. */
|
|
5
|
+
export function defineVelaConfig(config) {
|
|
6
|
+
return config;
|
|
7
|
+
}
|
|
8
|
+
const CANDIDATES = ['vela.config.js', 'vela.config.mjs', 'vela.config.ts'];
|
|
9
|
+
/**
|
|
10
|
+
* Locate + import the vela config. `.ts` requires a runtime that strips types
|
|
11
|
+
* (Node 22+ `--experimental-strip-types`, or tsx/ts-node); `.js`/`.mjs` load
|
|
12
|
+
* directly.
|
|
13
|
+
*/
|
|
14
|
+
export async function loadConfig(cwd = process.cwd(), explicitPath) {
|
|
15
|
+
const path = explicitPath
|
|
16
|
+
? isAbsolute(explicitPath)
|
|
17
|
+
? explicitPath
|
|
18
|
+
: resolve(cwd, explicitPath)
|
|
19
|
+
: await findConfig(cwd);
|
|
20
|
+
if (!path) {
|
|
21
|
+
throw new Error(`No vela config found. Create one of: ${CANDIDATES.join(', ')} (or pass --config <path>).`);
|
|
22
|
+
}
|
|
23
|
+
const mod = (await import(pathToFileURL(path).href));
|
|
24
|
+
const config = mod.default ?? mod.config;
|
|
25
|
+
if (!config || typeof config.createApp !== 'function') {
|
|
26
|
+
throw new Error(`Config at ${path} must export { createApp(): Promise<VelaApplication> } (default export or a named 'config').`);
|
|
27
|
+
}
|
|
28
|
+
return config;
|
|
29
|
+
}
|
|
30
|
+
async function findConfig(cwd) {
|
|
31
|
+
for (const name of CANDIDATES) {
|
|
32
|
+
const candidate = join(cwd, name);
|
|
33
|
+
try {
|
|
34
|
+
await access(candidate);
|
|
35
|
+
return candidate;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
// try next
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
package/dist/format.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SeederResult } from '@velajs/vela/seeder';
|
|
2
|
+
/**
|
|
3
|
+
* Render seeder results to a logger and return a process exit code
|
|
4
|
+
* (0 = all ran, 1 = at least one failed). Pure — no I/O beyond the logger.
|
|
5
|
+
*/
|
|
6
|
+
export declare function formatSeedResults(results: SeederResult[], log?: (message: string) => void): number;
|
|
7
|
+
/** Aligned plain-text table. Pure; returns lines. */
|
|
8
|
+
export declare function renderTable(headers: string[], rows: string[][]): string[];
|
package/dist/format.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render seeder results to a logger and return a process exit code
|
|
3
|
+
* (0 = all ran, 1 = at least one failed). Pure — no I/O beyond the logger.
|
|
4
|
+
*/
|
|
5
|
+
export function formatSeedResults(results, log = (m) => console.log(m)) {
|
|
6
|
+
if (results.length === 0) {
|
|
7
|
+
log('No seeders found.');
|
|
8
|
+
return 0;
|
|
9
|
+
}
|
|
10
|
+
let failed = 0;
|
|
11
|
+
for (const result of results) {
|
|
12
|
+
if (result.ok) {
|
|
13
|
+
log(` ✓ ${result.name}`);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
failed++;
|
|
17
|
+
log(` ✗ ${result.name}${result.error ? `: ${errorMessage(result.error)}` : ''}`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const total = results.length;
|
|
21
|
+
log(`\n${total - failed}/${total} seeders ran successfully.`);
|
|
22
|
+
return failed > 0 ? 1 : 0;
|
|
23
|
+
}
|
|
24
|
+
function errorMessage(error) {
|
|
25
|
+
return error instanceof Error ? error.message : String(error);
|
|
26
|
+
}
|
|
27
|
+
/** Aligned plain-text table. Pure; returns lines. */
|
|
28
|
+
export function renderTable(headers, rows) {
|
|
29
|
+
const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? '').length)));
|
|
30
|
+
const line = (cells) => cells.map((c, i) => (c ?? '').padEnd(widths[i])).join(' ').trimEnd();
|
|
31
|
+
return [line(headers), line(widths.map((w) => '-'.repeat(w))), ...rows.map(line)];
|
|
32
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
export { SeedCommand } from './commands/seed.command.js';
|
|
3
|
+
export { EntrypointListCommand, ModuleGraphCommand, OpenApiDumpCommand, RouteListCommand, } from './commands/introspect.commands.js';
|
|
4
|
+
export { collectRoutes, collectModules, collectEntrypoints, renderModuleTree } from './introspect.js';
|
|
5
|
+
export type { RouteRow, EntrypointRow } from './introspect.js';
|
|
6
|
+
export { renderTable } from './format.js';
|
|
7
|
+
export { loadConfig, defineVelaConfig } from './config.js';
|
|
8
|
+
export type { VelaConfig } from './config.js';
|
|
9
|
+
export { formatSeedResults } from './format.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Builtins, Cli } from 'clipanion';
|
|
3
|
+
import { EntrypointListCommand, ModuleGraphCommand, OpenApiDumpCommand, RouteListCommand, } from './commands/introspect.commands.js';
|
|
4
|
+
import { SeedCommand } from './commands/seed.command.js';
|
|
5
|
+
const cli = new Cli({
|
|
6
|
+
binaryName: 'vela',
|
|
7
|
+
binaryLabel: 'Vela CLI',
|
|
8
|
+
binaryVersion: '0.2.0',
|
|
9
|
+
});
|
|
10
|
+
cli.register(Builtins.HelpCommand);
|
|
11
|
+
cli.register(Builtins.VersionCommand);
|
|
12
|
+
cli.register(SeedCommand);
|
|
13
|
+
cli.register(RouteListCommand);
|
|
14
|
+
cli.register(ModuleGraphCommand);
|
|
15
|
+
cli.register(EntrypointListCommand);
|
|
16
|
+
cli.register(OpenApiDumpCommand);
|
|
17
|
+
void cli.runExit(process.argv.slice(2));
|
|
18
|
+
export { SeedCommand } from './commands/seed.command.js';
|
|
19
|
+
export { EntrypointListCommand, ModuleGraphCommand, OpenApiDumpCommand, RouteListCommand, } from './commands/introspect.commands.js';
|
|
20
|
+
export { collectRoutes, collectModules, collectEntrypoints, renderModuleTree } from './introspect.js';
|
|
21
|
+
export { renderTable } from './format.js';
|
|
22
|
+
export { loadConfig, defineVelaConfig } from './config.js';
|
|
23
|
+
export { formatSeedResults } from './format.js';
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { VelaApplication } from '@velajs/vela';
|
|
2
|
+
import type { ModuleDescription } from '@velajs/vela';
|
|
3
|
+
/** One row of `vela route list`. */
|
|
4
|
+
export interface RouteRow {
|
|
5
|
+
method: string;
|
|
6
|
+
path: string;
|
|
7
|
+
/** `Controller#handler`, or `(mounted)` for routes vela did not compose
|
|
8
|
+
* itself (RouteContributor/CRUD, OpenAPI UI mounts, manual Hono routes). */
|
|
9
|
+
handler: string;
|
|
10
|
+
source: 'controller' | 'mounted';
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The app's route table: `describeRoutes()` rows (framework-composed truth)
|
|
14
|
+
* plus everything else present on the Hono router, deduped and labeled
|
|
15
|
+
* `(mounted)`. Returns null when the app never built HTTP routes.
|
|
16
|
+
*/
|
|
17
|
+
export declare function collectRoutes(app: VelaApplication): RouteRow[] | null;
|
|
18
|
+
/** `vela module graph` tree lines (or raw descriptions for --json). */
|
|
19
|
+
export declare function collectModules(app: VelaApplication): ModuleDescription[];
|
|
20
|
+
export declare function renderModuleTree(modules: ModuleDescription[]): string[];
|
|
21
|
+
/** One row of `vela entrypoint list`. */
|
|
22
|
+
export interface EntrypointRow {
|
|
23
|
+
kind: string;
|
|
24
|
+
target: string;
|
|
25
|
+
meta: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Every DECLARED entrypoint kind (from the global kind store — includes kinds
|
|
29
|
+
* with zero entries) joined with the app's entries. Metadata-only entries of
|
|
30
|
+
* lazy modules list fine; nothing materializes.
|
|
31
|
+
*/
|
|
32
|
+
export declare function collectEntrypoints(app: VelaApplication): EntrypointRow[];
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { describeToken, getEntrypointKinds } from '@velajs/vela';
|
|
2
|
+
/**
|
|
3
|
+
* The app's route table: `describeRoutes()` rows (framework-composed truth)
|
|
4
|
+
* plus everything else present on the Hono router, deduped and labeled
|
|
5
|
+
* `(mounted)`. Returns null when the app never built HTTP routes.
|
|
6
|
+
*/
|
|
7
|
+
export function collectRoutes(app) {
|
|
8
|
+
let described;
|
|
9
|
+
try {
|
|
10
|
+
described = app.describeRoutes();
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return null; // no HTTP routes built (slim/non-HTTP app)
|
|
14
|
+
}
|
|
15
|
+
const rows = described.map((r) => ({
|
|
16
|
+
method: r.method,
|
|
17
|
+
path: r.path,
|
|
18
|
+
handler: `${r.controller}#${r.handler}`,
|
|
19
|
+
source: 'controller',
|
|
20
|
+
}));
|
|
21
|
+
const covered = new Set(described.map((r) => `${r.method} ${r.path}`));
|
|
22
|
+
for (const r of described) {
|
|
23
|
+
// @Head handlers are served by Hono under GET — claim that row too so it
|
|
24
|
+
// doesn't reappear as a mounted duplicate.
|
|
25
|
+
if (r.method === 'HEAD')
|
|
26
|
+
covered.add(`GET ${r.path}`);
|
|
27
|
+
}
|
|
28
|
+
const seenMounted = new Set();
|
|
29
|
+
for (const honoRoute of app.getHonoApp().routes) {
|
|
30
|
+
// 'ALL' entries are middleware mounts (framework-internal disposal/context
|
|
31
|
+
// wrappers, global + scoped middleware) — not endpoints.
|
|
32
|
+
if (honoRoute.method === 'ALL')
|
|
33
|
+
continue;
|
|
34
|
+
const key = `${honoRoute.method} ${honoRoute.path}`;
|
|
35
|
+
if (covered.has(key) || seenMounted.has(key))
|
|
36
|
+
continue;
|
|
37
|
+
seenMounted.add(key);
|
|
38
|
+
rows.push({ method: honoRoute.method, path: honoRoute.path, handler: '(mounted)', source: 'mounted' });
|
|
39
|
+
}
|
|
40
|
+
return rows.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
41
|
+
}
|
|
42
|
+
/** `vela module graph` tree lines (or raw descriptions for --json). */
|
|
43
|
+
export function collectModules(app) {
|
|
44
|
+
return app.getContainer().getModuleDescriptions();
|
|
45
|
+
}
|
|
46
|
+
export function renderModuleTree(modules) {
|
|
47
|
+
const byId = new Map(modules.map((m) => [m.moduleId, m]));
|
|
48
|
+
const imported = new Set(modules.flatMap((m) => m.imports));
|
|
49
|
+
const roots = modules.filter((m) => !imported.has(m.moduleId));
|
|
50
|
+
const lines = [];
|
|
51
|
+
const render = (id, depth, trail) => {
|
|
52
|
+
const mod = byId.get(id);
|
|
53
|
+
const flags = mod
|
|
54
|
+
? [mod.isGlobal ? 'global' : null, mod.lazy ? 'lazy' : null].filter(Boolean)
|
|
55
|
+
: [];
|
|
56
|
+
const suffix = flags.length > 0 ? ` (${flags.join(', ')})` : '';
|
|
57
|
+
const providers = mod ? ` — ${mod.providers.length} provider${mod.providers.length === 1 ? '' : 's'}` : '';
|
|
58
|
+
lines.push(`${' '.repeat(depth)}${id}${suffix}${providers}`);
|
|
59
|
+
if (!mod || trail.has(id))
|
|
60
|
+
return;
|
|
61
|
+
const nextTrail = new Set(trail).add(id);
|
|
62
|
+
for (const child of mod.imports)
|
|
63
|
+
render(child, depth + 1, nextTrail);
|
|
64
|
+
};
|
|
65
|
+
for (const root of roots)
|
|
66
|
+
render(root.moduleId, 0, new Set());
|
|
67
|
+
return lines;
|
|
68
|
+
}
|
|
69
|
+
function safeMeta(meta) {
|
|
70
|
+
try {
|
|
71
|
+
return JSON.stringify(meta, (_key, value) => typeof value === 'function'
|
|
72
|
+
? '[function]'
|
|
73
|
+
: typeof value === 'object' && value !== null && value.constructor !== Object && !Array.isArray(value)
|
|
74
|
+
? `[${value.constructor.name}]`
|
|
75
|
+
: value) ?? 'undefined';
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return '[unserializable]';
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Every DECLARED entrypoint kind (from the global kind store — includes kinds
|
|
83
|
+
* with zero entries) joined with the app's entries. Metadata-only entries of
|
|
84
|
+
* lazy modules list fine; nothing materializes.
|
|
85
|
+
*/
|
|
86
|
+
export function collectEntrypoints(app) {
|
|
87
|
+
const rows = [];
|
|
88
|
+
const declared = getEntrypointKinds().map((k) => k.kind);
|
|
89
|
+
const populated = app.entrypoints.kinds();
|
|
90
|
+
const kinds = [...new Set([...declared, ...populated])];
|
|
91
|
+
for (const kind of kinds) {
|
|
92
|
+
const entries = app.entrypoints.ofKind(kind);
|
|
93
|
+
if (entries.length === 0) {
|
|
94
|
+
rows.push({ kind, target: '(no entrypoints)', meta: '' });
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
for (const ep of entries) {
|
|
98
|
+
const method = ep.methodName !== undefined ? `#${String(ep.methodName)}` : '';
|
|
99
|
+
rows.push({ kind, target: `${describeToken(ep.token)}${method}`, meta: safeMeta(ep.meta) });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return rows;
|
|
103
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@velajs/cli",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "CLI for Vela apps — seeding and project tasks (Node-side; not bundled into the edge Worker)",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vela": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./config": {
|
|
17
|
+
"types": "./dist/config.d.ts",
|
|
18
|
+
"import": "./dist/config.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"clipanion": "^4.0.0-rc.4"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@velajs/vela": ">=1.15.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@swc/core": "^1.15.43",
|
|
36
|
+
"@types/node": "^22.0.0",
|
|
37
|
+
"@velajs/vela": "^1.15.0",
|
|
38
|
+
"typescript": "^6.0.3",
|
|
39
|
+
"unplugin-swc": "^1.5.9",
|
|
40
|
+
"vitest": "^4.1.9"
|
|
41
|
+
},
|
|
42
|
+
"keywords": [
|
|
43
|
+
"vela",
|
|
44
|
+
"cli",
|
|
45
|
+
"seeder",
|
|
46
|
+
"edge",
|
|
47
|
+
"cloudflare-workers"
|
|
48
|
+
],
|
|
49
|
+
"license": "MIT",
|
|
50
|
+
"scripts": {
|
|
51
|
+
"build": "rm -rf dist && tsc",
|
|
52
|
+
"typecheck": "tsc --noEmit",
|
|
53
|
+
"test": "vitest run"
|
|
54
|
+
}
|
|
55
|
+
}
|