@hazeljs/cli 0.2.0-rc.7 → 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/@template/src/hello.controller.ts +1 -1
- package/README.md +40 -3
- package/dist/commands/generate-agent.d.ts +2 -0
- package/dist/commands/generate-agent.js +13 -7
- package/dist/commands/generate-ai-service.d.ts +2 -0
- package/dist/commands/generate-ai-service.js +8 -2
- package/dist/commands/generate-app.d.ts +7 -0
- package/dist/commands/generate-app.interactive-packages.test.d.ts +1 -0
- package/dist/commands/generate-app.interactive-packages.test.js +107 -0
- package/dist/commands/generate-app.js +141 -0
- package/dist/commands/generate-app.new-command.test.d.ts +1 -0
- package/dist/commands/generate-app.new-command.test.js +86 -0
- package/dist/commands/generate-app.skeleton.test.d.ts +1 -0
- package/dist/commands/generate-app.skeleton.test.js +51 -0
- package/dist/commands/generate-auth.d.ts +2 -0
- package/dist/commands/generate-auth.js +39 -40
- package/dist/commands/generate-cache.d.ts +2 -0
- package/dist/commands/generate-cache.js +14 -12
- package/dist/commands/generate-config.d.ts +2 -0
- package/dist/commands/generate-config.js +14 -12
- package/dist/commands/generate-controller.d.ts +2 -0
- package/dist/commands/generate-controller.js +8 -2
- package/dist/commands/generate-cron.d.ts +2 -0
- package/dist/commands/generate-cron.js +14 -12
- package/dist/commands/generate-crud.d.ts +2 -0
- package/dist/commands/generate-crud.js +44 -33
- package/dist/commands/generate-discovery.d.ts +2 -0
- package/dist/commands/generate-discovery.js +14 -12
- package/dist/commands/generate-dto.d.ts +2 -0
- package/dist/commands/generate-dto.js +15 -6
- package/dist/commands/generate-exception-filter.d.ts +2 -0
- package/dist/commands/generate-exception-filter.js +8 -2
- package/dist/commands/generate-guard.d.ts +2 -0
- package/dist/commands/generate-guard.js +8 -2
- package/dist/commands/generate-interceptor.d.ts +2 -0
- package/dist/commands/generate-interceptor.js +8 -2
- package/dist/commands/generate-middleware.d.ts +2 -0
- package/dist/commands/generate-middleware.js +11 -10
- package/dist/commands/generate-module.d.ts +2 -0
- package/dist/commands/generate-module.js +35 -31
- package/dist/commands/generate-pipe.d.ts +2 -0
- package/dist/commands/generate-pipe.js +8 -2
- package/dist/commands/generate-rag.d.ts +2 -0
- package/dist/commands/generate-rag.js +14 -12
- package/dist/commands/generate-repository.d.ts +2 -0
- package/dist/commands/generate-repository.js +8 -2
- package/dist/commands/generate-serverless-handler.d.ts +2 -0
- package/dist/commands/generate-serverless-handler.js +14 -3
- package/dist/commands/generate-service.d.ts +2 -0
- package/dist/commands/generate-service.js +8 -2
- package/dist/commands/generate-setup.d.ts +4 -0
- package/dist/commands/generate-setup.js +187 -0
- package/dist/commands/generate-setup.test.d.ts +1 -0
- package/dist/commands/generate-setup.test.js +50 -0
- package/dist/commands/generate-websocket-gateway.d.ts +2 -0
- package/dist/commands/generate-websocket-gateway.js +8 -2
- package/dist/commands/info.test.d.ts +1 -0
- package/dist/commands/info.test.js +58 -0
- package/dist/commands/pdf-to-audio.test.d.ts +1 -0
- package/dist/commands/pdf-to-audio.test.js +92 -0
- package/dist/commands/utility-commands.test.d.ts +1 -0
- package/dist/commands/utility-commands.test.js +95 -0
- package/dist/index.js +32 -3
- package/dist/index.list.test.d.ts +0 -0
- package/dist/index.list.test.js +70 -0
- package/dist/utils/generator-registry.d.ts +15 -0
- package/dist/utils/generator-registry.js +97 -0
- package/dist/utils/generator-registry.test.d.ts +1 -0
- package/dist/utils/generator-registry.test.js +10 -0
- package/dist/utils/generator.d.ts +21 -1
- package/dist/utils/generator.js +23 -4
- package/dist/utils/generator.test.js +5 -4
- package/package.json +2 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const commander_1 = require("commander");
|
|
9
|
+
const info_1 = require("./info");
|
|
10
|
+
jest.mock('fs');
|
|
11
|
+
describe('infoCommand', () => {
|
|
12
|
+
const mockFs = fs_1.default;
|
|
13
|
+
let logSpy;
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
16
|
+
});
|
|
17
|
+
afterAll(() => {
|
|
18
|
+
logSpy.mockRestore();
|
|
19
|
+
});
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
jest.clearAllMocks();
|
|
22
|
+
mockFs.readdirSync.mockReturnValue(['a.ts', 'dir']);
|
|
23
|
+
mockFs.statSync.mockImplementation((p) => {
|
|
24
|
+
const s = String(p);
|
|
25
|
+
return { isDirectory: () => s.endsWith('dir') };
|
|
26
|
+
});
|
|
27
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({
|
|
28
|
+
name: 'demo',
|
|
29
|
+
version: '1.0.0',
|
|
30
|
+
description: 'desc',
|
|
31
|
+
dependencies: { '@hazeljs/core': '^0.2.0', lodash: '^4.0.0' },
|
|
32
|
+
devDependencies: { '@hazeljs/swagger': '^0.2.0' },
|
|
33
|
+
}));
|
|
34
|
+
mockFs.existsSync.mockImplementation((p) => {
|
|
35
|
+
const s = String(p);
|
|
36
|
+
if (s.endsWith(path_1.default.join(process.cwd(), 'package.json')))
|
|
37
|
+
return true;
|
|
38
|
+
if (s.endsWith(path_1.default.join(process.cwd(), 'src')))
|
|
39
|
+
return true;
|
|
40
|
+
// pretend some config files exist
|
|
41
|
+
if (s.endsWith('tsconfig.json'))
|
|
42
|
+
return true;
|
|
43
|
+
return false;
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
it('prints hazel packages and project structure', async () => {
|
|
47
|
+
const program = new commander_1.Command();
|
|
48
|
+
(0, info_1.infoCommand)(program);
|
|
49
|
+
await program.parseAsync(['info'], { from: 'user' });
|
|
50
|
+
const out = logSpy.mock.calls
|
|
51
|
+
.map((c) => c.map((x) => String(x)).join(' '))
|
|
52
|
+
.join('\n');
|
|
53
|
+
expect(out).toContain('Project Information');
|
|
54
|
+
expect(out).toContain('@hazeljs/core');
|
|
55
|
+
expect(out).toContain('@hazeljs/swagger');
|
|
56
|
+
expect(out).toContain('Project Structure');
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
9
|
+
const pdf_to_audio_1 = require("./pdf-to-audio");
|
|
10
|
+
jest.mock('fs');
|
|
11
|
+
jest.mock('form-data', () => {
|
|
12
|
+
return jest.fn().mockImplementation(() => ({
|
|
13
|
+
append: jest.fn(),
|
|
14
|
+
getHeaders: jest.fn(() => ({})),
|
|
15
|
+
}));
|
|
16
|
+
});
|
|
17
|
+
describe('pdf-to-audio', () => {
|
|
18
|
+
const mockFs = fs_1.default;
|
|
19
|
+
const MockFormData = form_data_1.default;
|
|
20
|
+
let exitSpy;
|
|
21
|
+
let logSpy;
|
|
22
|
+
let errSpy;
|
|
23
|
+
beforeAll(() => {
|
|
24
|
+
exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
25
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
26
|
+
errSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
27
|
+
});
|
|
28
|
+
afterAll(() => {
|
|
29
|
+
exitSpy.mockRestore();
|
|
30
|
+
logSpy.mockRestore();
|
|
31
|
+
errSpy.mockRestore();
|
|
32
|
+
});
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
global.fetch = jest.fn();
|
|
36
|
+
mockFs.createReadStream.mockReturnValue({});
|
|
37
|
+
});
|
|
38
|
+
it('convert: exits when file is not a PDF', async () => {
|
|
39
|
+
const program = new commander_1.Command();
|
|
40
|
+
(0, pdf_to_audio_1.pdfToAudioCommand)(program);
|
|
41
|
+
await program.parseAsync(['pdf-to-audio', 'convert', 'not-a-pdf.txt'], { from: 'user' });
|
|
42
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
43
|
+
});
|
|
44
|
+
it('convert: exits when file does not exist', async () => {
|
|
45
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
46
|
+
const program = new commander_1.Command();
|
|
47
|
+
(0, pdf_to_audio_1.pdfToAudioCommand)(program);
|
|
48
|
+
await program.parseAsync(['pdf-to-audio', 'convert', 'file.pdf'], { from: 'user' });
|
|
49
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
50
|
+
});
|
|
51
|
+
it('convert: submits job and prints job id (no wait)', async () => {
|
|
52
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
53
|
+
global.fetch.mockResolvedValue({
|
|
54
|
+
ok: true,
|
|
55
|
+
json: async () => ({ jobId: 'job-123' }),
|
|
56
|
+
});
|
|
57
|
+
const program = new commander_1.Command();
|
|
58
|
+
(0, pdf_to_audio_1.pdfToAudioCommand)(program);
|
|
59
|
+
await program.parseAsync(['pdf-to-audio', 'convert', 'file.pdf', '--api-url', 'http://x'], { from: 'user' });
|
|
60
|
+
expect(MockFormData).toHaveBeenCalled();
|
|
61
|
+
expect(global.fetch).toHaveBeenCalled();
|
|
62
|
+
const out = logSpy.mock.calls.map((c) => String(c[0])).join('\n');
|
|
63
|
+
expect(out).toContain('Job submitted: job-123');
|
|
64
|
+
});
|
|
65
|
+
it('status: prints download hint when completed without output', async () => {
|
|
66
|
+
global.fetch.mockResolvedValue({
|
|
67
|
+
ok: true,
|
|
68
|
+
json: async () => ({ jobId: 'job-1', status: 'completed', progress: 100 }),
|
|
69
|
+
});
|
|
70
|
+
const program = new commander_1.Command();
|
|
71
|
+
(0, pdf_to_audio_1.pdfToAudioCommand)(program);
|
|
72
|
+
await program.parseAsync(['pdf-to-audio', 'status', 'job-1', '--api-url', 'http://x'], { from: 'user' });
|
|
73
|
+
const out = logSpy.mock.calls.map((c) => String(c[0])).join('\n');
|
|
74
|
+
expect(out).toContain('Download: GET');
|
|
75
|
+
});
|
|
76
|
+
it('status: downloads audio when completed with output', async () => {
|
|
77
|
+
global.fetch
|
|
78
|
+
.mockResolvedValueOnce({
|
|
79
|
+
ok: true,
|
|
80
|
+
json: async () => ({ jobId: 'job-2', status: 'completed', progress: 100 }),
|
|
81
|
+
})
|
|
82
|
+
.mockResolvedValueOnce({
|
|
83
|
+
ok: true,
|
|
84
|
+
arrayBuffer: async () => new ArrayBuffer(3),
|
|
85
|
+
text: async () => '',
|
|
86
|
+
});
|
|
87
|
+
const program = new commander_1.Command();
|
|
88
|
+
(0, pdf_to_audio_1.pdfToAudioCommand)(program);
|
|
89
|
+
await program.parseAsync(['pdf-to-audio', 'status', 'job-2', '--api-url', 'http://x', '--output', 'out.mp3'], { from: 'user' });
|
|
90
|
+
expect(mockFs.writeFileSync).toHaveBeenCalledWith('out.mp3', expect.any(Buffer));
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const child_process_1 = require("child_process");
|
|
8
|
+
const commander_1 = require("commander");
|
|
9
|
+
const build_1 = require("./build");
|
|
10
|
+
const start_1 = require("./start");
|
|
11
|
+
const test_1 = require("./test");
|
|
12
|
+
const info_1 = require("./info");
|
|
13
|
+
jest.mock('fs');
|
|
14
|
+
jest.mock('child_process', () => ({ execSync: jest.fn() }));
|
|
15
|
+
describe('utility commands', () => {
|
|
16
|
+
const mockFs = fs_1.default;
|
|
17
|
+
const mockExec = child_process_1.execSync;
|
|
18
|
+
let exitSpy;
|
|
19
|
+
let logSpy;
|
|
20
|
+
let errSpy;
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => undefined);
|
|
23
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
24
|
+
errSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
25
|
+
});
|
|
26
|
+
afterAll(() => {
|
|
27
|
+
exitSpy.mockRestore();
|
|
28
|
+
logSpy.mockRestore();
|
|
29
|
+
errSpy.mockRestore();
|
|
30
|
+
});
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
jest.clearAllMocks();
|
|
33
|
+
mockExec.mockImplementation(() => undefined);
|
|
34
|
+
exitSpy.mockClear();
|
|
35
|
+
logSpy.mockClear();
|
|
36
|
+
errSpy.mockClear();
|
|
37
|
+
delete process.env.PORT;
|
|
38
|
+
});
|
|
39
|
+
it('build: exits when package.json missing', async () => {
|
|
40
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
41
|
+
const program = new commander_1.Command();
|
|
42
|
+
(0, build_1.buildCommand)(program);
|
|
43
|
+
await program.parseAsync(['build'], { from: 'user' });
|
|
44
|
+
expect(exitSpy).toHaveBeenCalled();
|
|
45
|
+
});
|
|
46
|
+
it('build: uses watch command when --watch is set', async () => {
|
|
47
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
48
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({ scripts: { build: 'tsc' } }));
|
|
49
|
+
const program = new commander_1.Command();
|
|
50
|
+
(0, build_1.buildCommand)(program);
|
|
51
|
+
await program.parseAsync(['build', '--watch'], { from: 'user' });
|
|
52
|
+
expect(mockExec).toHaveBeenCalledWith('npm run build -- --watch', { stdio: 'inherit' });
|
|
53
|
+
});
|
|
54
|
+
it('start: dev mode exits when dev script missing', async () => {
|
|
55
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
56
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({ scripts: { start: 'node dist/index.js' } }));
|
|
57
|
+
const program = new commander_1.Command();
|
|
58
|
+
(0, start_1.startCommand)(program);
|
|
59
|
+
await program.parseAsync(['start', '--dev'], { from: 'user' });
|
|
60
|
+
expect(exitSpy).toHaveBeenCalled();
|
|
61
|
+
});
|
|
62
|
+
it('start: sets PORT when provided', async () => {
|
|
63
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
64
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({ scripts: { start: 'node dist/index.js' } }));
|
|
65
|
+
const program = new commander_1.Command();
|
|
66
|
+
(0, start_1.startCommand)(program);
|
|
67
|
+
await program.parseAsync(['start', '--port', '7777'], { from: 'user' });
|
|
68
|
+
expect(process.env.PORT).toBe('7777');
|
|
69
|
+
expect(mockExec).toHaveBeenCalledWith('npm start', { stdio: 'inherit' });
|
|
70
|
+
});
|
|
71
|
+
it('test: uses test:ci when present', async () => {
|
|
72
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
73
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({ scripts: { test: 'jest', 'test:ci': 'jest --ci' } }));
|
|
74
|
+
const program = new commander_1.Command();
|
|
75
|
+
(0, test_1.testCommand)(program);
|
|
76
|
+
await program.parseAsync(['test', '--ci'], { from: 'user' });
|
|
77
|
+
expect(mockExec).toHaveBeenCalledWith('npm run test:ci', { stdio: 'inherit' });
|
|
78
|
+
});
|
|
79
|
+
it('test: adds pattern and coverage args when not in ci', async () => {
|
|
80
|
+
mockFs.existsSync.mockReturnValue(true);
|
|
81
|
+
mockFs.readFileSync.mockReturnValue(JSON.stringify({ scripts: { test: 'jest' } }));
|
|
82
|
+
const program = new commander_1.Command();
|
|
83
|
+
(0, test_1.testCommand)(program);
|
|
84
|
+
await program.parseAsync(['test', 'user.test', '--coverage'], { from: 'user' });
|
|
85
|
+
expect(mockExec).toHaveBeenCalledWith('npm test -- user.test --coverage', { stdio: 'inherit' });
|
|
86
|
+
});
|
|
87
|
+
it('info: returns early when package.json missing', async () => {
|
|
88
|
+
mockFs.existsSync.mockReturnValue(false);
|
|
89
|
+
const program = new commander_1.Command();
|
|
90
|
+
(0, info_1.infoCommand)(program);
|
|
91
|
+
await program.parseAsync(['info'], { from: 'user' });
|
|
92
|
+
expect(exitSpy).not.toHaveBeenCalled();
|
|
93
|
+
expect(logSpy).toHaveBeenCalled();
|
|
94
|
+
});
|
|
95
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -24,6 +24,8 @@ const generate_cache_1 = require("./commands/generate-cache");
|
|
|
24
24
|
const generate_cron_1 = require("./commands/generate-cron");
|
|
25
25
|
const generate_rag_1 = require("./commands/generate-rag");
|
|
26
26
|
const generate_discovery_1 = require("./commands/generate-discovery");
|
|
27
|
+
const generate_setup_1 = require("./commands/generate-setup");
|
|
28
|
+
const generator_registry_1 = require("./utils/generator-registry");
|
|
27
29
|
const info_1 = require("./commands/info");
|
|
28
30
|
const add_1 = require("./commands/add");
|
|
29
31
|
const build_1 = require("./commands/build");
|
|
@@ -44,11 +46,37 @@ program
|
|
|
44
46
|
(0, pdf_to_audio_1.pdfToAudioCommand)(program);
|
|
45
47
|
(0, start_1.startCommand)(program);
|
|
46
48
|
(0, test_1.testCommand)(program);
|
|
47
|
-
// Generate command group
|
|
49
|
+
// Generate command group (unified: hazel g <type> <name> [--path] [--dry-run] [--json], or hazel g --list)
|
|
48
50
|
const generateCommand = program
|
|
49
51
|
.command('generate')
|
|
50
|
-
.description('Generate HazelJS components')
|
|
51
|
-
.alias('g')
|
|
52
|
+
.description('Generate HazelJS components. Use: hazel g <type> <name> (e.g. hazel g controller users). Use --list to see all types.')
|
|
53
|
+
.alias('g')
|
|
54
|
+
.option('--list', 'List available generator types')
|
|
55
|
+
.option('--list-json', 'With --list: output list as JSON')
|
|
56
|
+
.action((options) => {
|
|
57
|
+
const outputJsonList = options.list && options.listJson;
|
|
58
|
+
if (options.list) {
|
|
59
|
+
if (outputJsonList) {
|
|
60
|
+
console.log(JSON.stringify({ generators: generator_registry_1.GENERATOR_LIST }));
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
console.log('\nAvailable generator types:\n');
|
|
64
|
+
generator_registry_1.GENERATOR_LIST.forEach((g) => {
|
|
65
|
+
const nameNote = g.nameRequired ? ' <name>' : ' [name]';
|
|
66
|
+
console.log(` ${g.type}${nameNote} ${g.description}`);
|
|
67
|
+
});
|
|
68
|
+
console.log('\nExample: hazel g controller users\n');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log('Usage: hazel g <type> <name> [--path <path>] [--dry-run] [--json]');
|
|
73
|
+
console.log(' hazel g --list List all generator types');
|
|
74
|
+
console.log(' hazel g --list --list-json List types as JSON');
|
|
75
|
+
console.log('Example: hazel g controller users\n');
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
// Skeleton app
|
|
79
|
+
(0, generate_app_1.registerGenerateApp)(generateCommand);
|
|
52
80
|
// Core components
|
|
53
81
|
(0, generate_controller_1.generateController)(generateCommand);
|
|
54
82
|
(0, generate_service_1.generateService)(generateCommand);
|
|
@@ -72,4 +100,5 @@ const generateCommand = program
|
|
|
72
100
|
(0, generate_cron_1.generateCron)(generateCommand);
|
|
73
101
|
(0, generate_rag_1.generateRag)(generateCommand);
|
|
74
102
|
(0, generate_discovery_1.generateDiscovery)(generateCommand);
|
|
103
|
+
(0, generate_setup_1.generateSetup)(generateCommand);
|
|
75
104
|
program.parse(process.argv);
|
|
File without changes
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Mock generators so requiring ./index doesn't do real work
|
|
3
|
+
jest.mock('./commands/generate-app', () => ({
|
|
4
|
+
generateApp: jest.fn(),
|
|
5
|
+
registerGenerateApp: jest.fn(),
|
|
6
|
+
}));
|
|
7
|
+
jest.mock('./commands/generate-controller', () => ({ generateController: jest.fn() }));
|
|
8
|
+
jest.mock('./commands/generate-service', () => ({ generateService: jest.fn() }));
|
|
9
|
+
jest.mock('./commands/generate-module', () => ({ generateModule: jest.fn() }));
|
|
10
|
+
jest.mock('./commands/generate-dto', () => ({ generateDto: jest.fn() }));
|
|
11
|
+
jest.mock('./commands/generate-guard', () => ({ generateGuard: jest.fn() }));
|
|
12
|
+
jest.mock('./commands/generate-interceptor', () => ({ generateInterceptor: jest.fn() }));
|
|
13
|
+
jest.mock('./commands/generate-middleware', () => ({ generateMiddleware: jest.fn() }));
|
|
14
|
+
jest.mock('./commands/generate-crud', () => ({ generateCrud: jest.fn() }));
|
|
15
|
+
jest.mock('./commands/generate-auth', () => ({ generateAuth: jest.fn() }));
|
|
16
|
+
jest.mock('./commands/generate-websocket-gateway', () => ({ generateWebSocketGateway: jest.fn() }));
|
|
17
|
+
jest.mock('./commands/generate-exception-filter', () => ({ generateExceptionFilter: jest.fn() }));
|
|
18
|
+
jest.mock('./commands/generate-pipe', () => ({ generatePipe: jest.fn() }));
|
|
19
|
+
jest.mock('./commands/generate-repository', () => ({ generateRepository: jest.fn() }));
|
|
20
|
+
jest.mock('./commands/generate-ai-service', () => ({ generateAIService: jest.fn() }));
|
|
21
|
+
jest.mock('./commands/generate-agent', () => ({ generateAgent: jest.fn() }));
|
|
22
|
+
jest.mock('./commands/generate-serverless-handler', () => ({ generateServerlessHandler: jest.fn() }));
|
|
23
|
+
jest.mock('./commands/generate-config', () => ({ generateConfig: jest.fn() }));
|
|
24
|
+
jest.mock('./commands/generate-cache', () => ({ generateCache: jest.fn() }));
|
|
25
|
+
jest.mock('./commands/generate-cron', () => ({ generateCron: jest.fn() }));
|
|
26
|
+
jest.mock('./commands/generate-rag', () => ({ generateRag: jest.fn() }));
|
|
27
|
+
jest.mock('./commands/generate-discovery', () => ({ generateDiscovery: jest.fn() }));
|
|
28
|
+
jest.mock('./commands/generate-setup', () => ({ generateSetup: jest.fn() }));
|
|
29
|
+
jest.mock('./commands/info', () => ({ infoCommand: jest.fn() }));
|
|
30
|
+
jest.mock('./commands/add', () => ({ addCommand: jest.fn() }));
|
|
31
|
+
jest.mock('./commands/build', () => ({ buildCommand: jest.fn() }));
|
|
32
|
+
jest.mock('./commands/pdf-to-audio', () => ({ pdfToAudioCommand: jest.fn() }));
|
|
33
|
+
jest.mock('./commands/start', () => ({ startCommand: jest.fn() }));
|
|
34
|
+
jest.mock('./commands/test', () => ({ testCommand: jest.fn() }));
|
|
35
|
+
describe('CLI generate --list', () => {
|
|
36
|
+
let logSpy;
|
|
37
|
+
beforeAll(() => {
|
|
38
|
+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
|
|
39
|
+
});
|
|
40
|
+
afterAll(() => {
|
|
41
|
+
logSpy.mockRestore();
|
|
42
|
+
});
|
|
43
|
+
beforeEach(() => {
|
|
44
|
+
jest.resetModules();
|
|
45
|
+
jest.clearAllMocks();
|
|
46
|
+
});
|
|
47
|
+
it('prints a human list with --list', () => {
|
|
48
|
+
process.argv = ['node', 'hazel', 'g', '--list'];
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
50
|
+
require('./index');
|
|
51
|
+
expect(logSpy).toHaveBeenCalled();
|
|
52
|
+
const output = logSpy.mock.calls.map((c) => String(c[0])).join('\n');
|
|
53
|
+
expect(output).toContain('Available generator types');
|
|
54
|
+
});
|
|
55
|
+
it('prints JSON with --list --list-json', () => {
|
|
56
|
+
process.argv = ['node', 'hazel', 'g', '--list', '--list-json'];
|
|
57
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
58
|
+
require('./index');
|
|
59
|
+
const output = logSpy.mock.calls.map((c) => String(c[0])).join('\n');
|
|
60
|
+
expect(output).toContain('{"generators":');
|
|
61
|
+
});
|
|
62
|
+
it('prints usage when no args provided to generate', () => {
|
|
63
|
+
// this triggers the action handler for generate without --list
|
|
64
|
+
process.argv = ['node', 'hazel', 'g'];
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
66
|
+
require('./index');
|
|
67
|
+
const output = logSpy.mock.calls.map((c) => String(c[0])).join('\n');
|
|
68
|
+
expect(output).toContain('Usage: hazel g <type> <name>');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GenerateResult, GenerateCLIOptions } from './generator';
|
|
2
|
+
export interface GeneratorMeta {
|
|
3
|
+
type: string;
|
|
4
|
+
description: string;
|
|
5
|
+
nameRequired: boolean;
|
|
6
|
+
options?: string[];
|
|
7
|
+
}
|
|
8
|
+
/** All available generator types and their metadata (for --list). */
|
|
9
|
+
export declare const GENERATOR_LIST: GeneratorMeta[];
|
|
10
|
+
/**
|
|
11
|
+
* Run a generator by type and name. Use for unified `hazel generate <type> <name>`.
|
|
12
|
+
* For types that don't require a name (auth, config), pass a placeholder (e.g. 'auth').
|
|
13
|
+
*/
|
|
14
|
+
export declare function runGenerator(type: string, name: string, options: GenerateCLIOptions): Promise<GenerateResult>;
|
|
15
|
+
export declare function getGeneratorTypes(): string[];
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.GENERATOR_LIST = void 0;
|
|
4
|
+
exports.runGenerator = runGenerator;
|
|
5
|
+
exports.getGeneratorTypes = getGeneratorTypes;
|
|
6
|
+
const generate_controller_1 = require("../commands/generate-controller");
|
|
7
|
+
const generate_service_1 = require("../commands/generate-service");
|
|
8
|
+
const generate_module_1 = require("../commands/generate-module");
|
|
9
|
+
const generate_dto_1 = require("../commands/generate-dto");
|
|
10
|
+
const generate_guard_1 = require("../commands/generate-guard");
|
|
11
|
+
const generate_interceptor_1 = require("../commands/generate-interceptor");
|
|
12
|
+
const generate_middleware_1 = require("../commands/generate-middleware");
|
|
13
|
+
const generate_crud_1 = require("../commands/generate-crud");
|
|
14
|
+
const generate_auth_1 = require("../commands/generate-auth");
|
|
15
|
+
const generate_websocket_gateway_1 = require("../commands/generate-websocket-gateway");
|
|
16
|
+
const generate_exception_filter_1 = require("../commands/generate-exception-filter");
|
|
17
|
+
const generate_pipe_1 = require("../commands/generate-pipe");
|
|
18
|
+
const generate_repository_1 = require("../commands/generate-repository");
|
|
19
|
+
const generate_ai_service_1 = require("../commands/generate-ai-service");
|
|
20
|
+
const generate_agent_1 = require("../commands/generate-agent");
|
|
21
|
+
const generate_serverless_handler_1 = require("../commands/generate-serverless-handler");
|
|
22
|
+
const generate_config_1 = require("../commands/generate-config");
|
|
23
|
+
const generate_cache_1 = require("../commands/generate-cache");
|
|
24
|
+
const generate_cron_1 = require("../commands/generate-cron");
|
|
25
|
+
const generate_rag_1 = require("../commands/generate-rag");
|
|
26
|
+
const generate_discovery_1 = require("../commands/generate-discovery");
|
|
27
|
+
const generate_app_1 = require("../commands/generate-app");
|
|
28
|
+
const generate_setup_1 = require("../commands/generate-setup");
|
|
29
|
+
/** All available generator types and their metadata (for --list). */
|
|
30
|
+
exports.GENERATOR_LIST = [
|
|
31
|
+
{ type: 'app', description: 'Skeleton HazelJS application (minimal template)', nameRequired: true, options: ['path'] },
|
|
32
|
+
{ type: 'setup', description: 'Minimal setup starter file for a HazelJS package', nameRequired: true, options: ['path'] },
|
|
33
|
+
{ type: 'controller', description: 'REST controller', nameRequired: true },
|
|
34
|
+
{ type: 'service', description: 'Service class', nameRequired: true },
|
|
35
|
+
{ type: 'module', description: 'Module with controller, service, DTOs', nameRequired: true },
|
|
36
|
+
{ type: 'dto', description: 'Create and update DTOs', nameRequired: true },
|
|
37
|
+
{ type: 'guard', description: 'Guard (e.g. auth)', nameRequired: true },
|
|
38
|
+
{ type: 'interceptor', description: 'Interceptor', nameRequired: true },
|
|
39
|
+
{ type: 'middleware', description: 'Middleware', nameRequired: true },
|
|
40
|
+
{ type: 'crud', description: 'Full CRUD resource (controller, service, module, DTOs)', nameRequired: true, options: ['route'] },
|
|
41
|
+
{ type: 'auth', description: 'Auth module (JWT guard, service, controller, DTOs)', nameRequired: false },
|
|
42
|
+
{ type: 'gateway', description: 'WebSocket gateway', nameRequired: true },
|
|
43
|
+
{ type: 'filter', description: 'Exception filter', nameRequired: true },
|
|
44
|
+
{ type: 'pipe', description: 'Validation/transform pipe', nameRequired: true },
|
|
45
|
+
{ type: 'repository', description: 'Prisma repository', nameRequired: true },
|
|
46
|
+
{ type: 'ai-service', description: 'AI service with decorators', nameRequired: true },
|
|
47
|
+
{ type: 'agent', description: 'AI agent with @Agent and @Tool', nameRequired: true },
|
|
48
|
+
{ type: 'serverless', description: 'Serverless handler (Lambda or Cloud Function)', nameRequired: true, options: ['platform'] },
|
|
49
|
+
{ type: 'config', description: 'Config module setup', nameRequired: false },
|
|
50
|
+
{ type: 'cache', description: 'Cache service with decorators', nameRequired: true },
|
|
51
|
+
{ type: 'cron', description: 'Cron/scheduled job service', nameRequired: true },
|
|
52
|
+
{ type: 'rag', description: 'RAG (Retrieval-Augmented Generation) service', nameRequired: true },
|
|
53
|
+
{ type: 'discovery', description: 'Service discovery setup', nameRequired: true },
|
|
54
|
+
];
|
|
55
|
+
const RUNNERS = {
|
|
56
|
+
app: generate_app_1.runApp,
|
|
57
|
+
setup: generate_setup_1.runSetup,
|
|
58
|
+
controller: generate_controller_1.runController,
|
|
59
|
+
service: generate_service_1.runService,
|
|
60
|
+
module: generate_module_1.runModule,
|
|
61
|
+
dto: generate_dto_1.runDto,
|
|
62
|
+
guard: generate_guard_1.runGuard,
|
|
63
|
+
interceptor: generate_interceptor_1.runInterceptor,
|
|
64
|
+
middleware: generate_middleware_1.runMiddleware,
|
|
65
|
+
crud: generate_crud_1.runCrud,
|
|
66
|
+
auth: generate_auth_1.runAuth,
|
|
67
|
+
gateway: generate_websocket_gateway_1.runWebSocketGateway,
|
|
68
|
+
filter: generate_exception_filter_1.runExceptionFilter,
|
|
69
|
+
pipe: generate_pipe_1.runPipe,
|
|
70
|
+
repository: generate_repository_1.runRepository,
|
|
71
|
+
'ai-service': generate_ai_service_1.runAIService,
|
|
72
|
+
agent: generate_agent_1.runAgent,
|
|
73
|
+
serverless: generate_serverless_handler_1.runServerlessHandler,
|
|
74
|
+
config: generate_config_1.runConfig,
|
|
75
|
+
cache: generate_cache_1.runCache,
|
|
76
|
+
cron: generate_cron_1.runCron,
|
|
77
|
+
rag: generate_rag_1.runRag,
|
|
78
|
+
discovery: generate_discovery_1.runDiscovery,
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Run a generator by type and name. Use for unified `hazel generate <type> <name>`.
|
|
82
|
+
* For types that don't require a name (auth, config), pass a placeholder (e.g. 'auth').
|
|
83
|
+
*/
|
|
84
|
+
async function runGenerator(type, name, options) {
|
|
85
|
+
const runner = RUNNERS[type];
|
|
86
|
+
if (!runner) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
created: [],
|
|
90
|
+
error: `Unknown generator type: "${type}". Use "hazel generate --list" to see available types.`,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
return runner(name, options);
|
|
94
|
+
}
|
|
95
|
+
function getGeneratorTypes() {
|
|
96
|
+
return exports.GENERATOR_LIST.map((g) => g.type);
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const generator_registry_1 = require("./generator-registry");
|
|
4
|
+
describe('generator-registry', () => {
|
|
5
|
+
it('returns a helpful error for unknown generator types', async () => {
|
|
6
|
+
const result = await (0, generator_registry_1.runGenerator)('nope', 'x', { dryRun: true });
|
|
7
|
+
expect(result.ok).toBe(false);
|
|
8
|
+
expect(result.error).toContain('Unknown generator type');
|
|
9
|
+
});
|
|
10
|
+
});
|
|
@@ -12,14 +12,34 @@ export interface GeneratorOptions {
|
|
|
12
12
|
data?: Record<string, unknown>;
|
|
13
13
|
dryRun?: boolean;
|
|
14
14
|
}
|
|
15
|
+
/** Result of a generation run for machine-readable output (e.g. --json) */
|
|
16
|
+
export interface GenerateResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
created: string[];
|
|
19
|
+
nextSteps?: string[];
|
|
20
|
+
dryRun?: boolean;
|
|
21
|
+
error?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Common CLI options for all generate commands */
|
|
24
|
+
export interface GenerateCLIOptions {
|
|
25
|
+
path?: string;
|
|
26
|
+
dryRun?: boolean;
|
|
27
|
+
json?: boolean;
|
|
28
|
+
route?: string;
|
|
29
|
+
platform?: string;
|
|
30
|
+
}
|
|
15
31
|
export declare class Generator {
|
|
16
32
|
/**
|
|
17
33
|
* File suffix appended before .ts (e.g. 'controller' -> name.controller.ts)
|
|
18
34
|
* Override in subclasses.
|
|
19
35
|
*/
|
|
20
36
|
protected suffix: string;
|
|
21
|
-
generate(options: GeneratorOptions): Promise<
|
|
37
|
+
generate(options: GeneratorOptions): Promise<GenerateResult>;
|
|
22
38
|
protected getFilePath(name: string, customPath?: string): string;
|
|
23
39
|
protected getDefaultTemplate(): string;
|
|
24
40
|
promptForOptions(options: Partial<GeneratorOptions>): Promise<GeneratorOptions>;
|
|
25
41
|
}
|
|
42
|
+
/** Output result for humans or as JSON. Use json: true to print a single JSON object to stdout. */
|
|
43
|
+
export declare function printGenerateResult(result: GenerateResult, options: {
|
|
44
|
+
json?: boolean;
|
|
45
|
+
}): void;
|
package/dist/utils/generator.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.toPascalCase = toPascalCase;
|
|
|
8
8
|
exports.toKebabCase = toKebabCase;
|
|
9
9
|
exports.toCamelCase = toCamelCase;
|
|
10
10
|
exports.renderTemplate = renderTemplate;
|
|
11
|
+
exports.printGenerateResult = printGenerateResult;
|
|
11
12
|
const fs_1 = __importDefault(require("fs"));
|
|
12
13
|
const path_1 = __importDefault(require("path"));
|
|
13
14
|
const chalk_1 = __importDefault(require("chalk"));
|
|
@@ -59,10 +60,9 @@ class Generator {
|
|
|
59
60
|
const content = mustache_1.default.render(templateContent, templateData);
|
|
60
61
|
// Determine the file path
|
|
61
62
|
const filePath = this.getFilePath(name, customPath);
|
|
63
|
+
const created = [filePath];
|
|
62
64
|
if (dryRun) {
|
|
63
|
-
|
|
64
|
-
console.log(chalk_1.default.gray(content));
|
|
65
|
-
return;
|
|
65
|
+
return { ok: true, created, dryRun: true };
|
|
66
66
|
}
|
|
67
67
|
// Create directory if it doesn't exist
|
|
68
68
|
const dir = path_1.default.dirname(filePath);
|
|
@@ -71,7 +71,7 @@ class Generator {
|
|
|
71
71
|
}
|
|
72
72
|
// Write the file
|
|
73
73
|
fs_1.default.writeFileSync(filePath, content);
|
|
74
|
-
|
|
74
|
+
return { ok: true, created };
|
|
75
75
|
}
|
|
76
76
|
getFilePath(name, customPath) {
|
|
77
77
|
const fileName = toKebabCase(name);
|
|
@@ -105,3 +105,22 @@ class Generator {
|
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
107
|
exports.Generator = Generator;
|
|
108
|
+
/** Output result for humans or as JSON. Use json: true to print a single JSON object to stdout. */
|
|
109
|
+
function printGenerateResult(result, options) {
|
|
110
|
+
if (options.json) {
|
|
111
|
+
console.log(JSON.stringify(result));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!result.ok) {
|
|
115
|
+
console.error(chalk_1.default.red(result.error ?? 'Generation failed'));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const prefix = result.dryRun ? chalk_1.default.blue('[dry-run] Would create ') : chalk_1.default.green('✓ Generated ');
|
|
119
|
+
for (const file of result.created) {
|
|
120
|
+
console.log(prefix + file);
|
|
121
|
+
}
|
|
122
|
+
if (result.nextSteps && result.nextSteps.length > 0) {
|
|
123
|
+
console.log(chalk_1.default.blue('\nNext steps:'));
|
|
124
|
+
result.nextSteps.forEach((step) => console.log(chalk_1.default.gray(' ' + step)));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -98,15 +98,16 @@ describe('Generator class', () => {
|
|
|
98
98
|
expect(mockFs.writeFileSync).toHaveBeenCalledWith(expect.any(String), 'MyWidget my-widget myWidget');
|
|
99
99
|
});
|
|
100
100
|
it('should not write files in dry-run mode', async () => {
|
|
101
|
-
const
|
|
102
|
-
await generator.generate({
|
|
101
|
+
const result = await generator.generate({
|
|
103
102
|
name: 'test',
|
|
104
103
|
template: 'content',
|
|
105
104
|
dryRun: true,
|
|
106
105
|
});
|
|
107
106
|
expect(mockFs.writeFileSync).not.toHaveBeenCalled();
|
|
108
|
-
expect(
|
|
109
|
-
|
|
107
|
+
expect(result.ok).toBe(true);
|
|
108
|
+
expect(result.dryRun).toBe(true);
|
|
109
|
+
expect(result.created).toHaveLength(1);
|
|
110
|
+
expect(result.created[0]).toContain('test.ts');
|
|
110
111
|
});
|
|
111
112
|
});
|
|
112
113
|
describe('promptForOptions', () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hazeljs/cli",
|
|
3
|
-
"version": "0.2.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Command-line interface for scaffolding and generating HazelJS applications and components",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"type": "opencollective",
|
|
70
70
|
"url": "https://opencollective.com/hazeljs"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "6f43ebe685dc190fbfd13f806976d58640197358"
|
|
73
73
|
}
|