@avijs/cli 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Christopher Robin Chase
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,13 @@
1
- # Temporary Holding Version
1
+ # @avijs/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The avi command: avi new, avi dev, avi check and avi manifest.
4
+
5
+ ```sh
6
+ npm install -D @avijs/cli
7
+ ```
8
+
9
+ Part of [Avi.js](https://github.com/chrischase011/avi-js), a TypeScript framework for Node.js with
10
+ modules, controllers and dependency injection, without `reflect-metadata`. See the
11
+ [tutorial](https://github.com/chrischase011/avi-js/blob/main/docs/tutorial/README.md) to get started.
12
+
13
+ Requires Node.js 22.12 or newer. MIT licensed.
package/dist/bin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.d.ts","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":""}
package/dist/bin.js ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './cli.js';
3
+ process.exitCode = await main(process.argv.slice(2));
4
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAEhC,OAAO,CAAC,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC"}
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ export declare function main(argv: readonly string[]): Promise<number>;
2
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AAwBA,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAcnE"}
package/dist/cli.js ADDED
@@ -0,0 +1,149 @@
1
+ import { main as compile } from '@avijs/compiler';
2
+ import { spawn, spawnSync } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+ import { mkdir, readdir, writeFile } from 'node:fs/promises';
5
+ import { createRequire } from 'node:module';
6
+ import path from 'node:path';
7
+ import { parseArgs } from 'node:util';
8
+ import { templateFiles } from './templates.js';
9
+ const { version } = createRequire(import.meta.url)('../package.json');
10
+ const USAGE = `Usage:
11
+ avi new <name> [--template modular|minimal] [--dir .] [--avi-version ^x.y.z] [--from <dir>]
12
+ Create a project. The modular template (default) uses modules and decorators;
13
+ minimal uses plain routes and services. --from points at a folder of packed
14
+ @avijs tarballs (pnpm pack) to install instead of the registry. --avi-version
15
+ defaults to this CLI's version.
16
+ avi dev [--project tsconfig.json] [--entry dist/main.js]
17
+ Compile, then recompile and restart on every change.
18
+ avi check [--project tsconfig.json]
19
+ Type-check every static inject list.
20
+ avi manifest <entry.js> [--export AppModule] [--out avi.manifest.json]
21
+ Describe the app's modules, providers and routes.`;
22
+ export async function main(argv) {
23
+ const [command, ...rest] = argv;
24
+ switch (command) {
25
+ case 'new':
26
+ return create(rest);
27
+ case 'dev':
28
+ return dev(rest);
29
+ case 'check':
30
+ case 'manifest':
31
+ return compile(argv);
32
+ default:
33
+ console.error(USAGE);
34
+ return command === undefined || command === 'help' || command === '--help' ? 0 : 2;
35
+ }
36
+ }
37
+ async function create(args) {
38
+ const { values, positionals } = parseArgs({
39
+ args: [...args],
40
+ allowPositionals: true,
41
+ options: {
42
+ template: { type: 'string', default: 'modular' },
43
+ dir: { type: 'string', default: '.' },
44
+ 'avi-version': { type: 'string', default: `^${version}` },
45
+ from: { type: 'string' },
46
+ },
47
+ });
48
+ const name = positionals[0];
49
+ if (!name || !/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
50
+ console.error('avi new needs a project name in lowercase letters, digits, ".", "_" or "-"');
51
+ return 2;
52
+ }
53
+ const template = values.template;
54
+ if (template !== 'modular' && template !== 'minimal') {
55
+ console.error(`Unknown template "${template}". Use modular or minimal.`);
56
+ return 2;
57
+ }
58
+ const target = path.resolve(values.dir, name);
59
+ if (existsSync(target) && (await readdir(target)).length > 0) {
60
+ console.error(`${target} already exists and is not empty`);
61
+ return 1;
62
+ }
63
+ const files = templateFiles({
64
+ name,
65
+ template,
66
+ aviVersion: values['avi-version'],
67
+ ...(values.from ? { localPackages: await localPackages(values.from, target) } : {}),
68
+ });
69
+ for (const [file, content] of Object.entries(files)) {
70
+ const destination = path.join(target, file);
71
+ await mkdir(path.dirname(destination), { recursive: true });
72
+ await writeFile(destination, content);
73
+ }
74
+ console.log(`Created ${name} (${template}) in ${target}
75
+
76
+ Next:
77
+ cd ${path.relative(process.cwd(), target) || '.'}
78
+ npm install
79
+ npm run dev`);
80
+ return 0;
81
+ }
82
+ async function dev(args) {
83
+ const { values } = parseArgs({
84
+ args: [...args],
85
+ options: {
86
+ project: { type: 'string', default: 'tsconfig.json' },
87
+ entry: { type: 'string', default: 'dist/main.js' },
88
+ 'dry-run': { type: 'boolean', default: false },
89
+ },
90
+ });
91
+ let tsc;
92
+ try {
93
+ tsc = createRequire(path.resolve('package.json')).resolve('typescript/bin/tsc');
94
+ }
95
+ catch {
96
+ console.error('avi dev needs TypeScript installed in this project: npm install -D typescript');
97
+ return 1;
98
+ }
99
+ const build = [tsc, '-p', values.project];
100
+ const watchBuild = [tsc, '-p', values.project, '--watch', '--preserveWatchOutput'];
101
+ const watchRun = ['--watch', '--watch-preserve-output', values.entry];
102
+ if (values['dry-run']) {
103
+ for (const command of [build, watchBuild, watchRun]) {
104
+ console.log(['node', ...command].join(' '));
105
+ }
106
+ return 0;
107
+ }
108
+ const first = spawnSync(process.execPath, build, { stdio: 'inherit' });
109
+ if (first.status !== 0)
110
+ return first.status ?? 1;
111
+ const children = [
112
+ spawn(process.execPath, watchBuild, { stdio: 'inherit' }),
113
+ spawn(process.execPath, watchRun, { stdio: 'inherit' }),
114
+ ];
115
+ return new Promise((resolve) => {
116
+ const stop = () => {
117
+ for (const child of children)
118
+ child.kill();
119
+ resolve(0);
120
+ };
121
+ process.once('SIGINT', stop);
122
+ process.once('SIGTERM', stop);
123
+ for (const child of children) {
124
+ child.once('exit', (code) => {
125
+ if (code !== null && code !== 0) {
126
+ for (const other of children)
127
+ other.kill();
128
+ resolve(code);
129
+ }
130
+ });
131
+ }
132
+ });
133
+ }
134
+ /** Maps each packed @avi tarball in `dir` to a `file:` spec relative to the new project. */
135
+ async function localPackages(dir, project) {
136
+ const folder = path.resolve(dir);
137
+ const specs = {};
138
+ for (const file of await readdir(folder)) {
139
+ const match = /^avijs-([a-z-]+?)-\d+\.\d+\.\d+.*\.tgz$/.exec(file);
140
+ if (!match?.[1])
141
+ continue;
142
+ const relative = path.relative(project, path.join(folder, file)).replaceAll(path.sep, '/');
143
+ specs[`@avijs/${match[1]}`] = `file:${relative}`;
144
+ }
145
+ if (!specs['@avijs/core'])
146
+ throw new Error(`No @avijs package tarballs found in ${folder}`);
147
+ return specs;
148
+ }
149
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,4 @@
1
+ export { main } from './cli.js';
2
+ export { templateFiles } from './templates.js';
3
+ export type { TemplateName, TemplateOptions } from './templates.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,YAAY,EAAE,YAAY,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { main } from './cli.js';
2
+ export { templateFiles } from './templates.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,UAAU,CAAC;AAChC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC"}
@@ -0,0 +1,16 @@
1
+ export type TemplateName = 'modular' | 'minimal';
2
+ export interface TemplateOptions {
3
+ name: string;
4
+ template: TemplateName;
5
+ /** Version range for the @avijs packages. */
6
+ aviVersion: string;
7
+ /**
8
+ * Local package files to use instead of the registry, keyed by package name, such as
9
+ * 'file:../artifacts/avijs-core-0.1.0.tgz'. Also added as npm overrides so packages that depend on
10
+ * each other resolve locally too.
11
+ */
12
+ localPackages?: Readonly<Record<string, string>>;
13
+ }
14
+ /** Every file of a new project, keyed by its path relative to the project folder. */
15
+ export declare function templateFiles(options: TemplateOptions): Record<string, string>;
16
+ //# sourceMappingURL=templates.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,YAAY,GAAG,SAAS,GAAG,SAAS,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,YAAY,CAAC;IACvB,6CAA6C;IAC7C,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;CAClD;AAED,qFAAqF;AACrF,wBAAgB,aAAa,CAAC,OAAO,EAAE,eAAe,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAK9E"}
@@ -0,0 +1,310 @@
1
+ /** Every file of a new project, keyed by its path relative to the project folder. */
2
+ export function templateFiles(options) {
3
+ return {
4
+ ...shared(options),
5
+ ...(options.template === 'modular' ? modular() : minimal()),
6
+ };
7
+ }
8
+ function shared({ name, template, aviVersion, localPackages, }) {
9
+ const spec = (pkg) => localPackages?.[pkg] ?? aviVersion;
10
+ const pkg = {
11
+ name,
12
+ version: '0.1.0',
13
+ private: true,
14
+ type: 'module',
15
+ scripts: {
16
+ build: 'tsc',
17
+ start: 'node dist/main.js',
18
+ dev: 'avi dev',
19
+ check: 'avi check',
20
+ test: 'tsc && node --test "dist/**/*.test.js"',
21
+ },
22
+ dependencies: { '@avijs/core': spec('@avijs/core') },
23
+ devDependencies: {
24
+ '@avijs/cli': spec('@avijs/cli'),
25
+ '@avijs/testing': spec('@avijs/testing'),
26
+ '@types/node': '^22.0.0',
27
+ typescript: '~6.0.3',
28
+ },
29
+ engines: { node: '>=22.12' },
30
+ ...(localPackages ? { overrides: localPackages } : {}),
31
+ };
32
+ const tsconfig = {
33
+ compilerOptions: {
34
+ target: 'ES2023',
35
+ lib: ['ES2023', 'ESNext.Disposable'],
36
+ module: 'NodeNext',
37
+ moduleResolution: 'NodeNext',
38
+ types: ['node'],
39
+ strict: true,
40
+ skipLibCheck: true,
41
+ outDir: 'dist',
42
+ rootDir: 'src',
43
+ sourceMap: true,
44
+ },
45
+ include: ['src'],
46
+ };
47
+ return {
48
+ 'package.json': `${JSON.stringify(pkg, null, 2)}\n`,
49
+ 'tsconfig.json': `${JSON.stringify(tsconfig, null, 2)}\n`,
50
+ '.gitignore': 'node_modules/\ndist/\n',
51
+ 'README.md': `# ${name}
52
+
53
+ An [Avi.js](https://github.com/chrischase011/avi-js) API (${template} template).
54
+
55
+ \`\`\`sh
56
+ npm install
57
+ npm run dev # compile and restart on change
58
+ npm test # compile and run the tests
59
+ npm start # run the compiled app
60
+ \`\`\`
61
+
62
+ Then open http://localhost:3000/health and http://localhost:3000/users.
63
+ `,
64
+ };
65
+ }
66
+ const MAIN_LISTEN = `const { port } = await app.listen(Number(process.env['PORT'] ?? 3000));
67
+ console.log(\`Listening on http://localhost:\${String(port)}\`);
68
+
69
+ const shutdown = () => {
70
+ void app.close().then(() => process.exit(0));
71
+ };
72
+ process.once('SIGINT', shutdown);
73
+ process.once('SIGTERM', shutdown);
74
+ `;
75
+ function modular() {
76
+ return {
77
+ 'src/main.ts': `import { AviFactory } from '@avijs/core';
78
+ import { AppModule } from './app.module.js';
79
+
80
+ const app = await AviFactory.create(AppModule);
81
+ ${MAIN_LISTEN}`,
82
+ 'src/app.module.ts': `import { Module } from '@avijs/core';
83
+ import { HealthController } from './common/health.controller.js';
84
+ import { UsersModule } from './modules/users/users.module.js';
85
+
86
+ @Module({ imports: [UsersModule], controllers: [HealthController] })
87
+ export class AppModule {}
88
+ `,
89
+ 'src/common/health.controller.ts': `import { Controller, Get } from '@avijs/core';
90
+
91
+ @Controller()
92
+ export class HealthController {
93
+ @Get('/health')
94
+ check() {
95
+ return 'ok';
96
+ }
97
+ }
98
+ `,
99
+ 'src/modules/users/users.module.ts': `import { Module } from '@avijs/core';
100
+ import { UsersController } from './users.controller.js';
101
+ import { UsersService } from './users.service.js';
102
+
103
+ @Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService] })
104
+ export class UsersModule {}
105
+ `,
106
+ 'src/modules/users/users.service.ts': `import { Injectable } from '@avijs/core';
107
+ import type { CreateUserDto } from './dto/create-user.dto.js';
108
+
109
+ export interface User {
110
+ id: number;
111
+ name: string;
112
+ }
113
+
114
+ @Injectable()
115
+ export class UsersService {
116
+ readonly #users: User[] = [{ id: 1, name: 'Ada' }];
117
+
118
+ findAll(): User[] {
119
+ return this.#users;
120
+ }
121
+
122
+ findOne(id: number): User | undefined {
123
+ return this.#users.find((user) => user.id === id);
124
+ }
125
+
126
+ create(dto: CreateUserDto): User {
127
+ const user = { id: this.#users.length + 1, name: dto.name };
128
+ this.#users.push(user);
129
+ return user;
130
+ }
131
+ }
132
+ `,
133
+ 'src/modules/users/users.controller.ts': `import { Controller, Get, HttpError, Post, reply, type Context } from '@avijs/core';
134
+ import { parseCreateUser } from './dto/create-user.dto.js';
135
+ import { UsersService } from './users.service.js';
136
+
137
+ @Controller('/users')
138
+ export class UsersController {
139
+ static inject = [UsersService] as const;
140
+
141
+ constructor(private readonly users: UsersService) {}
142
+
143
+ @Get()
144
+ findAll() {
145
+ return this.users.findAll();
146
+ }
147
+
148
+ @Get('/:id')
149
+ findOne(ctx: Context) {
150
+ const user = this.users.findOne(Number(ctx.params['id']));
151
+ if (!user) throw new HttpError(404, 'User not found');
152
+ return user;
153
+ }
154
+
155
+ @Post()
156
+ async create(ctx: Context) {
157
+ const dto = parseCreateUser(await ctx.json());
158
+ if (!dto) throw new HttpError(400, 'Body must be an object with a name');
159
+ return reply(this.users.create(dto), { status: 201 });
160
+ }
161
+ }
162
+ `,
163
+ 'src/modules/users/dto/create-user.dto.ts': `export interface CreateUserDto {
164
+ name: string;
165
+ }
166
+
167
+ // Swap this for a schema library (Zod, Valibot, ...) and route schemas when the API grows.
168
+ export function parseCreateUser(input: unknown): CreateUserDto | undefined {
169
+ if (typeof input !== 'object' || input === null) return undefined;
170
+ const { name } = input as { name?: unknown };
171
+ return typeof name === 'string' && name.trim() !== '' ? { name: name.trim() } : undefined;
172
+ }
173
+ `,
174
+ 'src/modules/users/users.test.ts': `import assert from 'node:assert/strict';
175
+ import { after, before, test } from 'node:test';
176
+ import { createTestClient, type TestClient } from '@avijs/testing';
177
+ import { AppModule } from '../../app.module.js';
178
+
179
+ let client: TestClient;
180
+ before(async () => {
181
+ client = await createTestClient(AppModule);
182
+ });
183
+ after(() => client.close());
184
+
185
+ test('lists users', async () => {
186
+ const res = await client.get('/users');
187
+ assert.equal(res.status, 200);
188
+ assert.deepEqual(res.body, [{ id: 1, name: 'Ada' }]);
189
+ });
190
+
191
+ test('creates a user', async () => {
192
+ const res = await client.post('/users', { name: 'Grace' });
193
+ assert.equal(res.status, 201);
194
+ assert.deepEqual(res.body, { id: 2, name: 'Grace' });
195
+ });
196
+
197
+ test('rejects a user without a name', async () => {
198
+ assert.equal((await client.post('/users', {})).status, 400);
199
+ });
200
+ `,
201
+ };
202
+ }
203
+ function minimal() {
204
+ return {
205
+ 'src/main.ts': `import { createApp } from './app.js';
206
+
207
+ const app = createApp();
208
+ ${MAIN_LISTEN}`,
209
+ 'src/app.ts': `import { createAvi } from '@avijs/core';
210
+ import { healthRoutes } from './routes/health.js';
211
+ import { usersRoutes } from './routes/users.js';
212
+ import { UsersService } from './services/users.js';
213
+
214
+ export function createApp() {
215
+ return createAvi().register({
216
+ providers: [UsersService],
217
+ controllers: [healthRoutes, usersRoutes],
218
+ });
219
+ }
220
+ `,
221
+ 'src/routes/health.ts': `import { defineController, route } from '@avijs/core';
222
+
223
+ export const healthRoutes = defineController({
224
+ routes: [route({ method: 'GET', path: '/health', handler: () => 'ok' })],
225
+ });
226
+ `,
227
+ 'src/routes/users.ts': `import { HttpError, defineController, reply, route } from '@avijs/core';
228
+ import { UsersService } from '../services/users.js';
229
+
230
+ export const usersRoutes = defineController({
231
+ prefix: '/users',
232
+ inject: [UsersService],
233
+ routes: (users) => [
234
+ route({ method: 'GET', path: '/', handler: () => users.findAll() }),
235
+ route({
236
+ method: 'GET',
237
+ path: '/:id',
238
+ handler: (ctx) => {
239
+ const user = users.findOne(Number(ctx.params.id));
240
+ if (!user) throw new HttpError(404, 'User not found');
241
+ return user;
242
+ },
243
+ }),
244
+ route({
245
+ method: 'POST',
246
+ path: '/',
247
+ handler: async (ctx) => {
248
+ const { name } = (await ctx.json()) as { name?: unknown };
249
+ if (typeof name !== 'string' || name.trim() === '') {
250
+ throw new HttpError(400, 'Body must be an object with a name');
251
+ }
252
+ return reply(users.create(name.trim()), { status: 201 });
253
+ },
254
+ }),
255
+ ],
256
+ });
257
+ `,
258
+ 'src/services/users.ts': `export interface User {
259
+ id: number;
260
+ name: string;
261
+ }
262
+
263
+ export class UsersService {
264
+ readonly #users: User[] = [{ id: 1, name: 'Ada' }];
265
+
266
+ findAll(): User[] {
267
+ return this.#users;
268
+ }
269
+
270
+ findOne(id: number): User | undefined {
271
+ return this.#users.find((user) => user.id === id);
272
+ }
273
+
274
+ create(name: string): User {
275
+ const user = { id: this.#users.length + 1, name };
276
+ this.#users.push(user);
277
+ return user;
278
+ }
279
+ }
280
+ `,
281
+ 'src/app.test.ts': `import assert from 'node:assert/strict';
282
+ import { after, before, test } from 'node:test';
283
+ import { createTestClient, type TestClient } from '@avijs/testing';
284
+ import { createApp } from './app.js';
285
+
286
+ let client: TestClient;
287
+ before(async () => {
288
+ client = await createTestClient(createApp());
289
+ });
290
+ after(() => client.close());
291
+
292
+ test('lists users', async () => {
293
+ const res = await client.get('/users');
294
+ assert.equal(res.status, 200);
295
+ assert.deepEqual(res.body, [{ id: 1, name: 'Ada' }]);
296
+ });
297
+
298
+ test('creates a user', async () => {
299
+ const res = await client.post('/users', { name: 'Grace' });
300
+ assert.equal(res.status, 201);
301
+ assert.deepEqual(res.body, { id: 2, name: 'Grace' });
302
+ });
303
+
304
+ test('answers 404 for an unknown user', async () => {
305
+ assert.equal((await client.get('/users/99')).status, 404);
306
+ });
307
+ `,
308
+ };
309
+ }
310
+ //# sourceMappingURL=templates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"templates.js","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,55 @@
1
1
  {
2
2
  "name": "@avijs/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "0.1.0",
4
+ "description": "The avi command: project scaffolding, dev mode and build checks.",
5
+ "keywords": [
6
+ "avi",
7
+ "avi.js",
8
+ "cli",
9
+ "scaffolding"
10
+ ],
11
+ "license": "MIT",
12
+ "author": "Christopher Robin Chase",
13
+ "homepage": "https://github.com/chrischase011/avi-js#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/chrischase011/avi-js/issues"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/chrischase011/avi-js.git",
20
+ "directory": "packages/cli"
21
+ },
22
+ "type": "module",
23
+ "engines": {
24
+ "node": ">=22.12"
25
+ },
26
+ "sideEffects": false,
27
+ "bin": {
28
+ "avi": "./dist/bin.js"
29
+ },
30
+ "exports": {
31
+ ".": {
32
+ "@avijs/source": "./src/index.ts",
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ }
36
+ },
37
+ "files": [
38
+ "dist",
39
+ "src",
40
+ "README.md",
41
+ "LICENSE"
42
+ ],
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "dependencies": {
47
+ "@avijs/compiler": "0.1.0"
48
+ },
49
+ "peerDependencies": {
50
+ "typescript": ">=5.8"
51
+ },
52
+ "scripts": {
53
+ "typecheck": "tsc -p tsconfig.json"
54
+ }
6
55
  }
package/src/bin.ts ADDED
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { main } from './cli.js';
3
+
4
+ process.exitCode = await main(process.argv.slice(2));
package/src/cli.ts ADDED
@@ -0,0 +1,155 @@
1
+ import { main as compile } from '@avijs/compiler';
2
+ import { spawn, spawnSync, type ChildProcess } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+ import { mkdir, readdir, writeFile } from 'node:fs/promises';
5
+ import { createRequire } from 'node:module';
6
+ import path from 'node:path';
7
+ import { parseArgs } from 'node:util';
8
+ import { templateFiles } from './templates.js';
9
+
10
+ const { version } = createRequire(import.meta.url)('../package.json') as { version: string };
11
+
12
+ const USAGE = `Usage:
13
+ avi new <name> [--template modular|minimal] [--dir .] [--avi-version ^x.y.z] [--from <dir>]
14
+ Create a project. The modular template (default) uses modules and decorators;
15
+ minimal uses plain routes and services. --from points at a folder of packed
16
+ @avijs tarballs (pnpm pack) to install instead of the registry. --avi-version
17
+ defaults to this CLI's version.
18
+ avi dev [--project tsconfig.json] [--entry dist/main.js]
19
+ Compile, then recompile and restart on every change.
20
+ avi check [--project tsconfig.json]
21
+ Type-check every static inject list.
22
+ avi manifest <entry.js> [--export AppModule] [--out avi.manifest.json]
23
+ Describe the app's modules, providers and routes.`;
24
+
25
+ export async function main(argv: readonly string[]): Promise<number> {
26
+ const [command, ...rest] = argv;
27
+ switch (command) {
28
+ case 'new':
29
+ return create(rest);
30
+ case 'dev':
31
+ return dev(rest);
32
+ case 'check':
33
+ case 'manifest':
34
+ return compile(argv);
35
+ default:
36
+ console.error(USAGE);
37
+ return command === undefined || command === 'help' || command === '--help' ? 0 : 2;
38
+ }
39
+ }
40
+
41
+ async function create(args: readonly string[]): Promise<number> {
42
+ const { values, positionals } = parseArgs({
43
+ args: [...args],
44
+ allowPositionals: true,
45
+ options: {
46
+ template: { type: 'string', default: 'modular' },
47
+ dir: { type: 'string', default: '.' },
48
+ 'avi-version': { type: 'string', default: `^${version}` },
49
+ from: { type: 'string' },
50
+ },
51
+ });
52
+ const name = positionals[0];
53
+ if (!name || !/^[a-z0-9][a-z0-9._-]*$/.test(name)) {
54
+ console.error('avi new needs a project name in lowercase letters, digits, ".", "_" or "-"');
55
+ return 2;
56
+ }
57
+ const template = values.template;
58
+ if (template !== 'modular' && template !== 'minimal') {
59
+ console.error(`Unknown template "${template}". Use modular or minimal.`);
60
+ return 2;
61
+ }
62
+
63
+ const target = path.resolve(values.dir, name);
64
+ if (existsSync(target) && (await readdir(target)).length > 0) {
65
+ console.error(`${target} already exists and is not empty`);
66
+ return 1;
67
+ }
68
+
69
+ const files = templateFiles({
70
+ name,
71
+ template,
72
+ aviVersion: values['avi-version'],
73
+ ...(values.from ? { localPackages: await localPackages(values.from, target) } : {}),
74
+ });
75
+ for (const [file, content] of Object.entries(files)) {
76
+ const destination = path.join(target, file);
77
+ await mkdir(path.dirname(destination), { recursive: true });
78
+ await writeFile(destination, content);
79
+ }
80
+
81
+ console.log(`Created ${name} (${template}) in ${target}
82
+
83
+ Next:
84
+ cd ${path.relative(process.cwd(), target) || '.'}
85
+ npm install
86
+ npm run dev`);
87
+ return 0;
88
+ }
89
+
90
+ async function dev(args: readonly string[]): Promise<number> {
91
+ const { values } = parseArgs({
92
+ args: [...args],
93
+ options: {
94
+ project: { type: 'string', default: 'tsconfig.json' },
95
+ entry: { type: 'string', default: 'dist/main.js' },
96
+ 'dry-run': { type: 'boolean', default: false },
97
+ },
98
+ });
99
+
100
+ let tsc: string;
101
+ try {
102
+ tsc = createRequire(path.resolve('package.json')).resolve('typescript/bin/tsc');
103
+ } catch {
104
+ console.error('avi dev needs TypeScript installed in this project: npm install -D typescript');
105
+ return 1;
106
+ }
107
+
108
+ const build = [tsc, '-p', values.project];
109
+ const watchBuild = [tsc, '-p', values.project, '--watch', '--preserveWatchOutput'];
110
+ const watchRun = ['--watch', '--watch-preserve-output', values.entry];
111
+ if (values['dry-run']) {
112
+ for (const command of [build, watchBuild, watchRun]) {
113
+ console.log(['node', ...command].join(' '));
114
+ }
115
+ return 0;
116
+ }
117
+
118
+ const first = spawnSync(process.execPath, build, { stdio: 'inherit' });
119
+ if (first.status !== 0) return first.status ?? 1;
120
+
121
+ const children: ChildProcess[] = [
122
+ spawn(process.execPath, watchBuild, { stdio: 'inherit' }),
123
+ spawn(process.execPath, watchRun, { stdio: 'inherit' }),
124
+ ];
125
+ return new Promise((resolve) => {
126
+ const stop = () => {
127
+ for (const child of children) child.kill();
128
+ resolve(0);
129
+ };
130
+ process.once('SIGINT', stop);
131
+ process.once('SIGTERM', stop);
132
+ for (const child of children) {
133
+ child.once('exit', (code) => {
134
+ if (code !== null && code !== 0) {
135
+ for (const other of children) other.kill();
136
+ resolve(code);
137
+ }
138
+ });
139
+ }
140
+ });
141
+ }
142
+
143
+ /** Maps each packed @avi tarball in `dir` to a `file:` spec relative to the new project. */
144
+ async function localPackages(dir: string, project: string): Promise<Record<string, string>> {
145
+ const folder = path.resolve(dir);
146
+ const specs: Record<string, string> = {};
147
+ for (const file of await readdir(folder)) {
148
+ const match = /^avijs-([a-z-]+?)-\d+\.\d+\.\d+.*\.tgz$/.exec(file);
149
+ if (!match?.[1]) continue;
150
+ const relative = path.relative(project, path.join(folder, file)).replaceAll(path.sep, '/');
151
+ specs[`@avijs/${match[1]}`] = `file:${relative}`;
152
+ }
153
+ if (!specs['@avijs/core']) throw new Error(`No @avijs package tarballs found in ${folder}`);
154
+ return specs;
155
+ }
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export { main } from './cli.js';
2
+ export { templateFiles } from './templates.js';
3
+ export type { TemplateName, TemplateOptions } from './templates.js';
@@ -0,0 +1,333 @@
1
+ export type TemplateName = 'modular' | 'minimal';
2
+
3
+ export interface TemplateOptions {
4
+ name: string;
5
+ template: TemplateName;
6
+ /** Version range for the @avijs packages. */
7
+ aviVersion: string;
8
+ /**
9
+ * Local package files to use instead of the registry, keyed by package name, such as
10
+ * 'file:../artifacts/avijs-core-0.1.0.tgz'. Also added as npm overrides so packages that depend on
11
+ * each other resolve locally too.
12
+ */
13
+ localPackages?: Readonly<Record<string, string>>;
14
+ }
15
+
16
+ /** Every file of a new project, keyed by its path relative to the project folder. */
17
+ export function templateFiles(options: TemplateOptions): Record<string, string> {
18
+ return {
19
+ ...shared(options),
20
+ ...(options.template === 'modular' ? modular() : minimal()),
21
+ };
22
+ }
23
+
24
+ function shared({
25
+ name,
26
+ template,
27
+ aviVersion,
28
+ localPackages,
29
+ }: TemplateOptions): Record<string, string> {
30
+ const spec = (pkg: string) => localPackages?.[pkg] ?? aviVersion;
31
+ const pkg = {
32
+ name,
33
+ version: '0.1.0',
34
+ private: true,
35
+ type: 'module',
36
+ scripts: {
37
+ build: 'tsc',
38
+ start: 'node dist/main.js',
39
+ dev: 'avi dev',
40
+ check: 'avi check',
41
+ test: 'tsc && node --test "dist/**/*.test.js"',
42
+ },
43
+ dependencies: { '@avijs/core': spec('@avijs/core') },
44
+ devDependencies: {
45
+ '@avijs/cli': spec('@avijs/cli'),
46
+ '@avijs/testing': spec('@avijs/testing'),
47
+ '@types/node': '^22.0.0',
48
+ typescript: '~6.0.3',
49
+ },
50
+ engines: { node: '>=22.12' },
51
+ ...(localPackages ? { overrides: localPackages } : {}),
52
+ };
53
+ const tsconfig = {
54
+ compilerOptions: {
55
+ target: 'ES2023',
56
+ lib: ['ES2023', 'ESNext.Disposable'],
57
+ module: 'NodeNext',
58
+ moduleResolution: 'NodeNext',
59
+ types: ['node'],
60
+ strict: true,
61
+ skipLibCheck: true,
62
+ outDir: 'dist',
63
+ rootDir: 'src',
64
+ sourceMap: true,
65
+ },
66
+ include: ['src'],
67
+ };
68
+ return {
69
+ 'package.json': `${JSON.stringify(pkg, null, 2)}\n`,
70
+ 'tsconfig.json': `${JSON.stringify(tsconfig, null, 2)}\n`,
71
+ '.gitignore': 'node_modules/\ndist/\n',
72
+ 'README.md': `# ${name}
73
+
74
+ An [Avi.js](https://github.com/chrischase011/avi-js) API (${template} template).
75
+
76
+ \`\`\`sh
77
+ npm install
78
+ npm run dev # compile and restart on change
79
+ npm test # compile and run the tests
80
+ npm start # run the compiled app
81
+ \`\`\`
82
+
83
+ Then open http://localhost:3000/health and http://localhost:3000/users.
84
+ `,
85
+ };
86
+ }
87
+
88
+ const MAIN_LISTEN = `const { port } = await app.listen(Number(process.env['PORT'] ?? 3000));
89
+ console.log(\`Listening on http://localhost:\${String(port)}\`);
90
+
91
+ const shutdown = () => {
92
+ void app.close().then(() => process.exit(0));
93
+ };
94
+ process.once('SIGINT', shutdown);
95
+ process.once('SIGTERM', shutdown);
96
+ `;
97
+
98
+ function modular(): Record<string, string> {
99
+ return {
100
+ 'src/main.ts': `import { AviFactory } from '@avijs/core';
101
+ import { AppModule } from './app.module.js';
102
+
103
+ const app = await AviFactory.create(AppModule);
104
+ ${MAIN_LISTEN}`,
105
+ 'src/app.module.ts': `import { Module } from '@avijs/core';
106
+ import { HealthController } from './common/health.controller.js';
107
+ import { UsersModule } from './modules/users/users.module.js';
108
+
109
+ @Module({ imports: [UsersModule], controllers: [HealthController] })
110
+ export class AppModule {}
111
+ `,
112
+ 'src/common/health.controller.ts': `import { Controller, Get } from '@avijs/core';
113
+
114
+ @Controller()
115
+ export class HealthController {
116
+ @Get('/health')
117
+ check() {
118
+ return 'ok';
119
+ }
120
+ }
121
+ `,
122
+ 'src/modules/users/users.module.ts': `import { Module } from '@avijs/core';
123
+ import { UsersController } from './users.controller.js';
124
+ import { UsersService } from './users.service.js';
125
+
126
+ @Module({ controllers: [UsersController], providers: [UsersService], exports: [UsersService] })
127
+ export class UsersModule {}
128
+ `,
129
+ 'src/modules/users/users.service.ts': `import { Injectable } from '@avijs/core';
130
+ import type { CreateUserDto } from './dto/create-user.dto.js';
131
+
132
+ export interface User {
133
+ id: number;
134
+ name: string;
135
+ }
136
+
137
+ @Injectable()
138
+ export class UsersService {
139
+ readonly #users: User[] = [{ id: 1, name: 'Ada' }];
140
+
141
+ findAll(): User[] {
142
+ return this.#users;
143
+ }
144
+
145
+ findOne(id: number): User | undefined {
146
+ return this.#users.find((user) => user.id === id);
147
+ }
148
+
149
+ create(dto: CreateUserDto): User {
150
+ const user = { id: this.#users.length + 1, name: dto.name };
151
+ this.#users.push(user);
152
+ return user;
153
+ }
154
+ }
155
+ `,
156
+ 'src/modules/users/users.controller.ts': `import { Controller, Get, HttpError, Post, reply, type Context } from '@avijs/core';
157
+ import { parseCreateUser } from './dto/create-user.dto.js';
158
+ import { UsersService } from './users.service.js';
159
+
160
+ @Controller('/users')
161
+ export class UsersController {
162
+ static inject = [UsersService] as const;
163
+
164
+ constructor(private readonly users: UsersService) {}
165
+
166
+ @Get()
167
+ findAll() {
168
+ return this.users.findAll();
169
+ }
170
+
171
+ @Get('/:id')
172
+ findOne(ctx: Context) {
173
+ const user = this.users.findOne(Number(ctx.params['id']));
174
+ if (!user) throw new HttpError(404, 'User not found');
175
+ return user;
176
+ }
177
+
178
+ @Post()
179
+ async create(ctx: Context) {
180
+ const dto = parseCreateUser(await ctx.json());
181
+ if (!dto) throw new HttpError(400, 'Body must be an object with a name');
182
+ return reply(this.users.create(dto), { status: 201 });
183
+ }
184
+ }
185
+ `,
186
+ 'src/modules/users/dto/create-user.dto.ts': `export interface CreateUserDto {
187
+ name: string;
188
+ }
189
+
190
+ // Swap this for a schema library (Zod, Valibot, ...) and route schemas when the API grows.
191
+ export function parseCreateUser(input: unknown): CreateUserDto | undefined {
192
+ if (typeof input !== 'object' || input === null) return undefined;
193
+ const { name } = input as { name?: unknown };
194
+ return typeof name === 'string' && name.trim() !== '' ? { name: name.trim() } : undefined;
195
+ }
196
+ `,
197
+ 'src/modules/users/users.test.ts': `import assert from 'node:assert/strict';
198
+ import { after, before, test } from 'node:test';
199
+ import { createTestClient, type TestClient } from '@avijs/testing';
200
+ import { AppModule } from '../../app.module.js';
201
+
202
+ let client: TestClient;
203
+ before(async () => {
204
+ client = await createTestClient(AppModule);
205
+ });
206
+ after(() => client.close());
207
+
208
+ test('lists users', async () => {
209
+ const res = await client.get('/users');
210
+ assert.equal(res.status, 200);
211
+ assert.deepEqual(res.body, [{ id: 1, name: 'Ada' }]);
212
+ });
213
+
214
+ test('creates a user', async () => {
215
+ const res = await client.post('/users', { name: 'Grace' });
216
+ assert.equal(res.status, 201);
217
+ assert.deepEqual(res.body, { id: 2, name: 'Grace' });
218
+ });
219
+
220
+ test('rejects a user without a name', async () => {
221
+ assert.equal((await client.post('/users', {})).status, 400);
222
+ });
223
+ `,
224
+ };
225
+ }
226
+
227
+ function minimal(): Record<string, string> {
228
+ return {
229
+ 'src/main.ts': `import { createApp } from './app.js';
230
+
231
+ const app = createApp();
232
+ ${MAIN_LISTEN}`,
233
+ 'src/app.ts': `import { createAvi } from '@avijs/core';
234
+ import { healthRoutes } from './routes/health.js';
235
+ import { usersRoutes } from './routes/users.js';
236
+ import { UsersService } from './services/users.js';
237
+
238
+ export function createApp() {
239
+ return createAvi().register({
240
+ providers: [UsersService],
241
+ controllers: [healthRoutes, usersRoutes],
242
+ });
243
+ }
244
+ `,
245
+ 'src/routes/health.ts': `import { defineController, route } from '@avijs/core';
246
+
247
+ export const healthRoutes = defineController({
248
+ routes: [route({ method: 'GET', path: '/health', handler: () => 'ok' })],
249
+ });
250
+ `,
251
+ 'src/routes/users.ts': `import { HttpError, defineController, reply, route } from '@avijs/core';
252
+ import { UsersService } from '../services/users.js';
253
+
254
+ export const usersRoutes = defineController({
255
+ prefix: '/users',
256
+ inject: [UsersService],
257
+ routes: (users) => [
258
+ route({ method: 'GET', path: '/', handler: () => users.findAll() }),
259
+ route({
260
+ method: 'GET',
261
+ path: '/:id',
262
+ handler: (ctx) => {
263
+ const user = users.findOne(Number(ctx.params.id));
264
+ if (!user) throw new HttpError(404, 'User not found');
265
+ return user;
266
+ },
267
+ }),
268
+ route({
269
+ method: 'POST',
270
+ path: '/',
271
+ handler: async (ctx) => {
272
+ const { name } = (await ctx.json()) as { name?: unknown };
273
+ if (typeof name !== 'string' || name.trim() === '') {
274
+ throw new HttpError(400, 'Body must be an object with a name');
275
+ }
276
+ return reply(users.create(name.trim()), { status: 201 });
277
+ },
278
+ }),
279
+ ],
280
+ });
281
+ `,
282
+ 'src/services/users.ts': `export interface User {
283
+ id: number;
284
+ name: string;
285
+ }
286
+
287
+ export class UsersService {
288
+ readonly #users: User[] = [{ id: 1, name: 'Ada' }];
289
+
290
+ findAll(): User[] {
291
+ return this.#users;
292
+ }
293
+
294
+ findOne(id: number): User | undefined {
295
+ return this.#users.find((user) => user.id === id);
296
+ }
297
+
298
+ create(name: string): User {
299
+ const user = { id: this.#users.length + 1, name };
300
+ this.#users.push(user);
301
+ return user;
302
+ }
303
+ }
304
+ `,
305
+ 'src/app.test.ts': `import assert from 'node:assert/strict';
306
+ import { after, before, test } from 'node:test';
307
+ import { createTestClient, type TestClient } from '@avijs/testing';
308
+ import { createApp } from './app.js';
309
+
310
+ let client: TestClient;
311
+ before(async () => {
312
+ client = await createTestClient(createApp());
313
+ });
314
+ after(() => client.close());
315
+
316
+ test('lists users', async () => {
317
+ const res = await client.get('/users');
318
+ assert.equal(res.status, 200);
319
+ assert.deepEqual(res.body, [{ id: 1, name: 'Ada' }]);
320
+ });
321
+
322
+ test('creates a user', async () => {
323
+ const res = await client.post('/users', { name: 'Grace' });
324
+ assert.equal(res.status, 201);
325
+ assert.deepEqual(res.body, { id: 2, name: 'Grace' });
326
+ });
327
+
328
+ test('answers 404 for an unknown user', async () => {
329
+ assert.equal((await client.get('/users/99')).status, 404);
330
+ });
331
+ `,
332
+ };
333
+ }