@hexabot-ai/cli 3.0.0-alpha.3
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/.prettierrc +5 -0
- package/AGENTS.md +64 -0
- package/README.md +192 -0
- package/dist/cli.js +31 -0
- package/dist/commands/__tests__/check.test.js +97 -0
- package/dist/commands/__tests__/config.test.js +80 -0
- package/dist/commands/__tests__/dev.test.js +105 -0
- package/dist/commands/__tests__/docker.test.js +132 -0
- package/dist/commands/__tests__/env.test.js +72 -0
- package/dist/commands/__tests__/migrate.test.js +42 -0
- package/dist/commands/__tests__/start.test.js +120 -0
- package/dist/commands/check.js +73 -0
- package/dist/commands/config.js +76 -0
- package/dist/commands/create.js +131 -0
- package/dist/commands/dev.js +72 -0
- package/dist/commands/docker.js +119 -0
- package/dist/commands/env.js +44 -0
- package/dist/commands/migrate.js +22 -0
- package/dist/commands/start.js +76 -0
- package/dist/core/__tests__/config.test.js +88 -0
- package/dist/core/__tests__/docker.test.js +43 -0
- package/dist/core/__tests__/env.test.js +71 -0
- package/dist/core/__tests__/package-manager.test.js +95 -0
- package/dist/core/__tests__/project.test.js +49 -0
- package/dist/core/config.js +78 -0
- package/dist/core/docker.js +66 -0
- package/dist/core/env.js +50 -0
- package/dist/core/package-manager.js +87 -0
- package/dist/core/prerequisites.js +80 -0
- package/dist/core/project.js +58 -0
- package/dist/index.js +16 -0
- package/dist/services/templates.js +27 -0
- package/dist/ui/banner.js +14 -0
- package/dist/utils/__tests__/services.test.js +18 -0
- package/dist/utils/__tests__/validation.test.js +17 -0
- package/dist/utils/__tests__/version.test.js +27 -0
- package/dist/utils/services.js +11 -0
- package/dist/utils/validation.js +9 -0
- package/dist/utils/version.js +22 -0
- package/eslint.config-staged.cjs +10 -0
- package/eslint.config.cjs +104 -0
- package/jest.config.ts +24 -0
- package/package.json +63 -0
- package/src/cli.ts +37 -0
- package/src/commands/__tests__/check.test.ts +116 -0
- package/src/commands/__tests__/config.test.ts +97 -0
- package/src/commands/__tests__/dev.test.ts +151 -0
- package/src/commands/__tests__/docker.test.ts +168 -0
- package/src/commands/__tests__/env.test.ts +95 -0
- package/src/commands/__tests__/migrate.test.ts +64 -0
- package/src/commands/__tests__/start.test.ts +166 -0
- package/src/commands/check.ts +102 -0
- package/src/commands/config.ts +90 -0
- package/src/commands/create.ts +201 -0
- package/src/commands/dev.ts +122 -0
- package/src/commands/docker.ts +190 -0
- package/src/commands/env.ts +62 -0
- package/src/commands/migrate.ts +27 -0
- package/src/commands/start.ts +126 -0
- package/src/core/__tests__/config.test.ts +114 -0
- package/src/core/__tests__/docker.test.ts +59 -0
- package/src/core/__tests__/env.test.ts +97 -0
- package/src/core/__tests__/package-manager.test.ts +121 -0
- package/src/core/__tests__/project.test.ts +68 -0
- package/src/core/config.ts +127 -0
- package/src/core/docker.ts +91 -0
- package/src/core/env.ts +90 -0
- package/src/core/package-manager.ts +126 -0
- package/src/core/prerequisites.ts +117 -0
- package/src/core/project.ts +97 -0
- package/src/index.ts +21 -0
- package/src/services/templates.ts +33 -0
- package/src/ui/banner.ts +18 -0
- package/src/utils/__tests__/services.test.ts +21 -0
- package/src/utils/__tests__/validation.test.ts +21 -0
- package/src/utils/__tests__/version.test.ts +35 -0
- package/src/utils/services.ts +12 -0
- package/src/utils/validation.ts +11 -0
- package/src/utils/version.ts +28 -0
- package/test/__mocks__/chalk.ts +13 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import { jest } from '@jest/globals';
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
const loadProjectConfig = jest.fn();
|
|
9
|
+
const dockerCompose = jest.fn();
|
|
10
|
+
const generateComposeFiles = jest.fn();
|
|
11
|
+
const resolveComposeFile = jest.fn();
|
|
12
|
+
const bootstrapEnvFile = jest.fn();
|
|
13
|
+
const checkDocker = jest.fn();
|
|
14
|
+
const assertHexabotProject = jest.fn();
|
|
15
|
+
const ensureDockerFolder = jest.fn();
|
|
16
|
+
const parseServices = jest.fn();
|
|
17
|
+
const runStart = jest.fn();
|
|
18
|
+
jest.unstable_mockModule('../../core/config.js', () => ({
|
|
19
|
+
loadProjectConfig,
|
|
20
|
+
}));
|
|
21
|
+
jest.unstable_mockModule('../../core/docker.js', () => ({
|
|
22
|
+
dockerCompose,
|
|
23
|
+
generateComposeFiles,
|
|
24
|
+
resolveComposeFile,
|
|
25
|
+
}));
|
|
26
|
+
jest.unstable_mockModule('../../core/env.js', () => ({
|
|
27
|
+
bootstrapEnvFile,
|
|
28
|
+
}));
|
|
29
|
+
jest.unstable_mockModule('../../core/prerequisites.js', () => ({
|
|
30
|
+
checkDocker,
|
|
31
|
+
}));
|
|
32
|
+
jest.unstable_mockModule('../../core/project.js', () => ({
|
|
33
|
+
assertHexabotProject,
|
|
34
|
+
ensureDockerFolder,
|
|
35
|
+
}));
|
|
36
|
+
jest.unstable_mockModule('../../utils/services.js', () => ({
|
|
37
|
+
parseServices,
|
|
38
|
+
}));
|
|
39
|
+
jest.unstable_mockModule('../start.js', () => ({
|
|
40
|
+
runStart,
|
|
41
|
+
}));
|
|
42
|
+
let registerDockerCommand;
|
|
43
|
+
beforeAll(async () => {
|
|
44
|
+
({ registerDockerCommand } = await import('../docker.js'));
|
|
45
|
+
});
|
|
46
|
+
beforeEach(() => {
|
|
47
|
+
jest.resetAllMocks();
|
|
48
|
+
parseServices.mockImplementation((value) => (typeof value === 'string' ? value : '')
|
|
49
|
+
.split(',')
|
|
50
|
+
.filter((entry) => entry));
|
|
51
|
+
resolveComposeFile.mockReturnValue('/project/docker/docker-compose.yml');
|
|
52
|
+
generateComposeFiles.mockReturnValue('-f docker-compose.yml');
|
|
53
|
+
loadProjectConfig.mockReturnValue({
|
|
54
|
+
docker: {
|
|
55
|
+
composeFile: 'docker/docker-compose.yml',
|
|
56
|
+
defaultServices: ['api'],
|
|
57
|
+
},
|
|
58
|
+
env: {
|
|
59
|
+
docker: '.env.docker',
|
|
60
|
+
dockerExample: '.env.docker.example',
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
const createProgram = () => {
|
|
65
|
+
const program = new Command();
|
|
66
|
+
registerDockerCommand(program);
|
|
67
|
+
return program;
|
|
68
|
+
};
|
|
69
|
+
describe('registerDockerCommand', () => {
|
|
70
|
+
it('runs docker compose up with extra flags and services', async () => {
|
|
71
|
+
const program = createProgram();
|
|
72
|
+
await program.parseAsync([
|
|
73
|
+
'node',
|
|
74
|
+
'test',
|
|
75
|
+
'docker',
|
|
76
|
+
'up',
|
|
77
|
+
'--services',
|
|
78
|
+
'api,postgres',
|
|
79
|
+
'--build',
|
|
80
|
+
'--detach',
|
|
81
|
+
]);
|
|
82
|
+
expect(bootstrapEnvFile).toHaveBeenCalled();
|
|
83
|
+
expect(generateComposeFiles).toHaveBeenCalledWith('/project/docker/docker-compose.yml', ['api', 'postgres'], 'dev');
|
|
84
|
+
expect(dockerCompose).toHaveBeenCalledWith(expect.stringContaining('up --build -d'));
|
|
85
|
+
});
|
|
86
|
+
it('runs docker compose down with optional volume removal', async () => {
|
|
87
|
+
const program = createProgram();
|
|
88
|
+
await program.parseAsync(['node', 'test', 'docker', 'down', '--volumes']);
|
|
89
|
+
expect(dockerCompose).toHaveBeenCalledWith(expect.stringContaining('down -v'));
|
|
90
|
+
expect(bootstrapEnvFile).not.toHaveBeenCalled();
|
|
91
|
+
});
|
|
92
|
+
it('tails docker logs with filters', async () => {
|
|
93
|
+
const program = createProgram();
|
|
94
|
+
await program.parseAsync([
|
|
95
|
+
'node',
|
|
96
|
+
'test',
|
|
97
|
+
'docker',
|
|
98
|
+
'logs',
|
|
99
|
+
'api',
|
|
100
|
+
'--follow',
|
|
101
|
+
'--since',
|
|
102
|
+
'1h',
|
|
103
|
+
]);
|
|
104
|
+
expect(dockerCompose).toHaveBeenCalledWith(expect.stringContaining('logs -f --since 1h api'));
|
|
105
|
+
});
|
|
106
|
+
it('lists running services with docker ps', async () => {
|
|
107
|
+
const program = createProgram();
|
|
108
|
+
await program.parseAsync(['node', 'test', 'docker', 'ps']);
|
|
109
|
+
expect(dockerCompose).toHaveBeenCalledWith(expect.stringContaining('ps'));
|
|
110
|
+
});
|
|
111
|
+
it('delegates to start command for docker start', async () => {
|
|
112
|
+
const program = createProgram();
|
|
113
|
+
await program.parseAsync([
|
|
114
|
+
'node',
|
|
115
|
+
'test',
|
|
116
|
+
'docker',
|
|
117
|
+
'start',
|
|
118
|
+
'--services',
|
|
119
|
+
'api',
|
|
120
|
+
'--build',
|
|
121
|
+
'--detach',
|
|
122
|
+
'--env-bootstrap',
|
|
123
|
+
]);
|
|
124
|
+
expect(runStart).toHaveBeenCalledWith({
|
|
125
|
+
docker: true,
|
|
126
|
+
services: 'api',
|
|
127
|
+
build: true,
|
|
128
|
+
detach: true,
|
|
129
|
+
envBootstrap: true,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import { jest } from '@jest/globals';
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
const loadProjectConfig = jest.fn();
|
|
9
|
+
const bootstrapEnvFile = jest.fn();
|
|
10
|
+
const listEnvStatus = jest.fn();
|
|
11
|
+
const assertHexabotProject = jest.fn();
|
|
12
|
+
jest.unstable_mockModule('../../core/config.js', () => ({
|
|
13
|
+
loadProjectConfig,
|
|
14
|
+
}));
|
|
15
|
+
jest.unstable_mockModule('../../core/env.js', () => ({
|
|
16
|
+
bootstrapEnvFile,
|
|
17
|
+
listEnvStatus,
|
|
18
|
+
}));
|
|
19
|
+
jest.unstable_mockModule('../../core/project.js', () => ({
|
|
20
|
+
assertHexabotProject,
|
|
21
|
+
}));
|
|
22
|
+
let registerEnvCommand;
|
|
23
|
+
beforeAll(async () => {
|
|
24
|
+
({ registerEnvCommand } = await import('../env.js'));
|
|
25
|
+
});
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
jest.resetAllMocks();
|
|
28
|
+
loadProjectConfig.mockReturnValue({
|
|
29
|
+
env: {
|
|
30
|
+
local: '.env',
|
|
31
|
+
localExample: '.env.example',
|
|
32
|
+
docker: '.env.docker',
|
|
33
|
+
dockerExample: '.env.docker.example',
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('registerEnvCommand', () => {
|
|
38
|
+
it('bootstraps env files for docker or local targets', async () => {
|
|
39
|
+
const dockerProgram = new Command();
|
|
40
|
+
registerEnvCommand(dockerProgram);
|
|
41
|
+
await dockerProgram.parseAsync([
|
|
42
|
+
'node',
|
|
43
|
+
'test',
|
|
44
|
+
'env',
|
|
45
|
+
'init',
|
|
46
|
+
'--docker',
|
|
47
|
+
'--force',
|
|
48
|
+
]);
|
|
49
|
+
expect(bootstrapEnvFile).toHaveBeenCalledWith(expect.any(String), '.env.docker.example', '.env.docker', { force: true });
|
|
50
|
+
jest.clearAllMocks();
|
|
51
|
+
const localProgram = new Command();
|
|
52
|
+
registerEnvCommand(localProgram);
|
|
53
|
+
await localProgram.parseAsync(['node', 'test', 'env', 'init']);
|
|
54
|
+
expect(bootstrapEnvFile).toHaveBeenCalledWith(expect.any(String), '.env.example', '.env', { force: undefined });
|
|
55
|
+
});
|
|
56
|
+
it('lists env statuses with formatted output', async () => {
|
|
57
|
+
const statuses = [
|
|
58
|
+
{ file: '.env', exists: true },
|
|
59
|
+
{ file: '.env.docker', exists: false },
|
|
60
|
+
];
|
|
61
|
+
listEnvStatus.mockReturnValue(statuses);
|
|
62
|
+
const logs = [];
|
|
63
|
+
jest.spyOn(console, 'log').mockImplementation((message) => {
|
|
64
|
+
logs.push(String(message));
|
|
65
|
+
});
|
|
66
|
+
const program = new Command();
|
|
67
|
+
registerEnvCommand(program);
|
|
68
|
+
await program.parseAsync(['node', 'test', 'env', 'list']);
|
|
69
|
+
expect(logs.join('\n')).toContain('.env');
|
|
70
|
+
expect(logs.join('\n')).toContain('.env.docker');
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import { jest } from '@jest/globals';
|
|
7
|
+
import { Command } from 'commander';
|
|
8
|
+
const dockerExec = jest.fn();
|
|
9
|
+
const ensureDockerFolder = jest.fn();
|
|
10
|
+
const checkDocker = jest.fn();
|
|
11
|
+
jest.unstable_mockModule('../../core/docker.js', () => ({
|
|
12
|
+
dockerExec,
|
|
13
|
+
}));
|
|
14
|
+
jest.unstable_mockModule('../../core/project.js', () => ({
|
|
15
|
+
ensureDockerFolder,
|
|
16
|
+
}));
|
|
17
|
+
jest.unstable_mockModule('../../core/prerequisites.js', () => ({
|
|
18
|
+
checkDocker,
|
|
19
|
+
}));
|
|
20
|
+
let registerMigrateCommand;
|
|
21
|
+
beforeAll(async () => {
|
|
22
|
+
({ registerMigrateCommand } = await import('../migrate.js'));
|
|
23
|
+
});
|
|
24
|
+
describe('registerMigrateCommand', () => {
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
jest.clearAllMocks();
|
|
27
|
+
ensureDockerFolder.mockReturnValue('/workspace/docker');
|
|
28
|
+
});
|
|
29
|
+
it('runs migrations with additional arguments', async () => {
|
|
30
|
+
const program = new Command();
|
|
31
|
+
registerMigrateCommand(program);
|
|
32
|
+
await program.parseAsync(['node', 'test', 'migrate', 'seed', '20241128']);
|
|
33
|
+
expect(ensureDockerFolder).toHaveBeenCalled();
|
|
34
|
+
expect(dockerExec).toHaveBeenCalledWith('api', 'npm run migrate seed 20241128', '--user $(id -u):$(id -g)');
|
|
35
|
+
});
|
|
36
|
+
it('runs migrations without extra arguments', async () => {
|
|
37
|
+
const program = new Command();
|
|
38
|
+
registerMigrateCommand(program);
|
|
39
|
+
await program.parseAsync(['node', 'test', 'migrate']);
|
|
40
|
+
expect(dockerExec).toHaveBeenCalledWith('api', 'npm run migrate', '--user $(id -u):$(id -g)');
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import { jest } from '@jest/globals';
|
|
7
|
+
const loadProjectConfig = jest.fn();
|
|
8
|
+
const updateProjectConfig = jest.fn();
|
|
9
|
+
const dockerCompose = jest.fn();
|
|
10
|
+
const generateComposeFiles = jest.fn();
|
|
11
|
+
const resolveComposeFile = jest.fn();
|
|
12
|
+
const bootstrapEnvFile = jest.fn();
|
|
13
|
+
const resolveEnvExample = jest.fn();
|
|
14
|
+
const detectPackageManager = jest.fn();
|
|
15
|
+
const normalizePackageManager = jest.fn();
|
|
16
|
+
const runPackageScript = jest.fn();
|
|
17
|
+
const checkDocker = jest.fn();
|
|
18
|
+
const assertHexabotProject = jest.fn();
|
|
19
|
+
const ensureDockerFolder = jest.fn();
|
|
20
|
+
jest.unstable_mockModule('../../core/config.js', () => ({
|
|
21
|
+
loadProjectConfig,
|
|
22
|
+
updateProjectConfig,
|
|
23
|
+
}));
|
|
24
|
+
jest.unstable_mockModule('../../core/docker.js', () => ({
|
|
25
|
+
dockerCompose,
|
|
26
|
+
generateComposeFiles,
|
|
27
|
+
resolveComposeFile,
|
|
28
|
+
}));
|
|
29
|
+
jest.unstable_mockModule('../../core/env.js', () => ({
|
|
30
|
+
bootstrapEnvFile,
|
|
31
|
+
resolveEnvExample,
|
|
32
|
+
}));
|
|
33
|
+
jest.unstable_mockModule('../../core/package-manager.js', () => ({
|
|
34
|
+
detectPackageManager,
|
|
35
|
+
normalizePackageManager,
|
|
36
|
+
runPackageScript,
|
|
37
|
+
}));
|
|
38
|
+
jest.unstable_mockModule('../../core/prerequisites.js', () => ({
|
|
39
|
+
checkDocker,
|
|
40
|
+
}));
|
|
41
|
+
jest.unstable_mockModule('../../core/project.js', () => ({
|
|
42
|
+
assertHexabotProject,
|
|
43
|
+
ensureDockerFolder,
|
|
44
|
+
}));
|
|
45
|
+
jest.unstable_mockModule('../../utils/services.js', () => ({
|
|
46
|
+
parseServices: (value) => value.split(',').filter(Boolean),
|
|
47
|
+
}));
|
|
48
|
+
let runStart;
|
|
49
|
+
beforeAll(async () => {
|
|
50
|
+
({ runStart } = await import('../start.js'));
|
|
51
|
+
});
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
jest.resetAllMocks();
|
|
54
|
+
normalizePackageManager.mockImplementation((value) => value);
|
|
55
|
+
});
|
|
56
|
+
describe('runStart', () => {
|
|
57
|
+
const baseConfig = {
|
|
58
|
+
startScript: 'start',
|
|
59
|
+
packageManager: 'npm',
|
|
60
|
+
docker: {
|
|
61
|
+
composeFile: 'docker/docker-compose.yml',
|
|
62
|
+
defaultServices: [],
|
|
63
|
+
},
|
|
64
|
+
env: {
|
|
65
|
+
local: '.env',
|
|
66
|
+
localExample: '.env.example',
|
|
67
|
+
docker: '.env.docker',
|
|
68
|
+
dockerExample: '.env.docker.example',
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
it('runs the npm start script when not using Docker', async () => {
|
|
72
|
+
loadProjectConfig.mockReturnValue(baseConfig);
|
|
73
|
+
await runStart();
|
|
74
|
+
expect(assertHexabotProject).toHaveBeenCalled();
|
|
75
|
+
expect(runPackageScript).toHaveBeenCalledWith('npm', 'start', expect.any(String));
|
|
76
|
+
expect(detectPackageManager).not.toHaveBeenCalled();
|
|
77
|
+
expect(updateProjectConfig).not.toHaveBeenCalled();
|
|
78
|
+
expect(dockerCompose).not.toHaveBeenCalled();
|
|
79
|
+
});
|
|
80
|
+
it('runs docker compose in prod mode with env bootstrap', async () => {
|
|
81
|
+
const config = {
|
|
82
|
+
...baseConfig,
|
|
83
|
+
packageManager: undefined,
|
|
84
|
+
docker: {
|
|
85
|
+
composeFile: 'docker/docker-compose.yml',
|
|
86
|
+
defaultServices: ['api'],
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
loadProjectConfig.mockReturnValue(config);
|
|
90
|
+
detectPackageManager.mockReturnValue('pnpm');
|
|
91
|
+
resolveComposeFile.mockReturnValue('/tmp/docker/docker-compose.yml');
|
|
92
|
+
generateComposeFiles.mockReturnValue('-f docker-compose.yml');
|
|
93
|
+
await runStart({
|
|
94
|
+
docker: true,
|
|
95
|
+
services: 'api,postgres',
|
|
96
|
+
envBootstrap: true,
|
|
97
|
+
build: true,
|
|
98
|
+
detach: true,
|
|
99
|
+
});
|
|
100
|
+
expect(detectPackageManager).toHaveBeenCalled();
|
|
101
|
+
expect(updateProjectConfig).toHaveBeenCalledWith(expect.any(String), {
|
|
102
|
+
packageManager: 'pnpm',
|
|
103
|
+
});
|
|
104
|
+
expect(bootstrapEnvFile).toHaveBeenCalledWith(expect.any(String), config.env.dockerExample, config.env.docker);
|
|
105
|
+
expect(resolveEnvExample).not.toHaveBeenCalled();
|
|
106
|
+
expect(checkDocker).toHaveBeenCalled();
|
|
107
|
+
expect(ensureDockerFolder).toHaveBeenCalled();
|
|
108
|
+
expect(generateComposeFiles).toHaveBeenCalledWith('/tmp/docker/docker-compose.yml', ['api', 'postgres'], 'prod');
|
|
109
|
+
expect(dockerCompose).toHaveBeenCalledWith(expect.stringContaining('up --build -d'));
|
|
110
|
+
expect(runPackageScript).not.toHaveBeenCalled();
|
|
111
|
+
});
|
|
112
|
+
it('bootstraps envs for local start when requested', async () => {
|
|
113
|
+
loadProjectConfig.mockReturnValue(baseConfig);
|
|
114
|
+
resolveEnvExample.mockReturnValue('.env.local.example');
|
|
115
|
+
await runStart({ envBootstrap: true, env: '.env.local' });
|
|
116
|
+
expect(resolveEnvExample).toHaveBeenCalledWith(expect.any(String), '.env.local', baseConfig.env.localExample);
|
|
117
|
+
expect(bootstrapEnvFile).toHaveBeenCalledWith(expect.any(String), '.env.local.example', '.env.local');
|
|
118
|
+
expect(runPackageScript).toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { loadProjectConfig } from '../core/config.js';
|
|
8
|
+
import { listEnvStatus } from '../core/env.js';
|
|
9
|
+
import { checkDocker, checkNodeVersion } from '../core/prerequisites.js';
|
|
10
|
+
import { isHexabotProject } from '../core/project.js';
|
|
11
|
+
export const registerCheckCommand = (program) => {
|
|
12
|
+
program
|
|
13
|
+
.command('check')
|
|
14
|
+
.description('Run diagnostic checks')
|
|
15
|
+
.option('--docker-only', 'Only run Docker checks')
|
|
16
|
+
.option('--no-docker', 'Skip Docker checks')
|
|
17
|
+
.action((options) => {
|
|
18
|
+
runDiagnostics({
|
|
19
|
+
dockerOnly: options.dockerOnly,
|
|
20
|
+
noDocker: options.noDocker ?? options.docker === false,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
};
|
|
24
|
+
const runDiagnostics = (options) => {
|
|
25
|
+
const projectRoot = process.cwd();
|
|
26
|
+
const onlyDocker = options.dockerOnly;
|
|
27
|
+
const skipDocker = options.noDocker;
|
|
28
|
+
const results = [];
|
|
29
|
+
if (!onlyDocker) {
|
|
30
|
+
const nodeResult = checkNodeVersion({ fatal: false, silent: true });
|
|
31
|
+
results.push({
|
|
32
|
+
label: 'Node.js version',
|
|
33
|
+
ok: nodeResult.ok,
|
|
34
|
+
message: nodeResult.message,
|
|
35
|
+
});
|
|
36
|
+
const projectResult = isHexabotProject(projectRoot);
|
|
37
|
+
results.push({
|
|
38
|
+
label: 'Hexabot project',
|
|
39
|
+
ok: projectResult,
|
|
40
|
+
message: projectResult
|
|
41
|
+
? 'Found @hexabot-ai/api in package.json'
|
|
42
|
+
: 'Run inside a Hexabot project directory',
|
|
43
|
+
});
|
|
44
|
+
if (projectResult) {
|
|
45
|
+
const config = loadProjectConfig(projectRoot);
|
|
46
|
+
const envStatuses = listEnvStatus(projectRoot, config);
|
|
47
|
+
envStatuses.forEach((status) => {
|
|
48
|
+
results.push({
|
|
49
|
+
label: `Env file ${status.file}`,
|
|
50
|
+
ok: status.exists,
|
|
51
|
+
message: status.exists ? 'Found' : 'Missing',
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!skipDocker) {
|
|
57
|
+
const dockerResult = checkDocker({ fatal: false, silent: true });
|
|
58
|
+
results.push({
|
|
59
|
+
label: 'Docker',
|
|
60
|
+
ok: dockerResult.ok,
|
|
61
|
+
message: dockerResult.message,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const hasErrors = results.some((result) => !result.ok);
|
|
65
|
+
results.forEach((result) => printResult(result));
|
|
66
|
+
if (hasErrors) {
|
|
67
|
+
process.exitCode = 1;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
const printResult = (result) => {
|
|
71
|
+
const status = result.ok ? chalk.green('PASS') : chalk.red('FAIL');
|
|
72
|
+
console.log(`${status} ${result.label}${result.message ? ` — ${result.message}` : ''}`);
|
|
73
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { loadProjectConfig, updateProjectConfig } from '../core/config.js';
|
|
9
|
+
import { normalizePackageManager } from '../core/package-manager.js';
|
|
10
|
+
import { assertHexabotProject } from '../core/project.js';
|
|
11
|
+
export const registerConfigCommand = (program) => {
|
|
12
|
+
const configCommand = program.command('config').description('Project config');
|
|
13
|
+
configCommand
|
|
14
|
+
.command('show')
|
|
15
|
+
.description('Print effective Hexabot configuration')
|
|
16
|
+
.action(() => {
|
|
17
|
+
const projectRoot = path.resolve(process.cwd());
|
|
18
|
+
assertHexabotProject(projectRoot);
|
|
19
|
+
const config = loadProjectConfig(projectRoot);
|
|
20
|
+
console.log(JSON.stringify(config, null, 2));
|
|
21
|
+
});
|
|
22
|
+
configCommand
|
|
23
|
+
.command('set <key> <value>')
|
|
24
|
+
.description('Update a configuration value (dot notation)')
|
|
25
|
+
.action((key, value) => {
|
|
26
|
+
const projectRoot = path.resolve(process.cwd());
|
|
27
|
+
assertHexabotProject(projectRoot);
|
|
28
|
+
const normalizedValue = parseValue(key, value);
|
|
29
|
+
const override = buildOverride(key.split('.'), normalizedValue);
|
|
30
|
+
const updated = updateProjectConfig(projectRoot, override);
|
|
31
|
+
console.log(chalk.green('Updated hexabot.config.json'));
|
|
32
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
33
|
+
});
|
|
34
|
+
};
|
|
35
|
+
const parseValue = (key, value) => {
|
|
36
|
+
if (key === 'packageManager') {
|
|
37
|
+
return normalizePackageManager(value);
|
|
38
|
+
}
|
|
39
|
+
const trimmed = value.trim();
|
|
40
|
+
if (trimmed === 'true') {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
if (trimmed === 'false') {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
if (!Number.isNaN(Number(trimmed)) && trimmed !== '') {
|
|
47
|
+
return Number(trimmed);
|
|
48
|
+
}
|
|
49
|
+
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(trimmed);
|
|
52
|
+
}
|
|
53
|
+
catch (_error) {
|
|
54
|
+
// fallback to string
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (trimmed.includes(',') && key.endsWith('Services')) {
|
|
58
|
+
return trimmed
|
|
59
|
+
.split(',')
|
|
60
|
+
.map((entry) => entry.trim())
|
|
61
|
+
.filter(Boolean);
|
|
62
|
+
}
|
|
63
|
+
return trimmed;
|
|
64
|
+
};
|
|
65
|
+
const buildOverride = (parts, value) => {
|
|
66
|
+
if (!parts.length) {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
const [head, ...rest] = parts;
|
|
70
|
+
if (!head) {
|
|
71
|
+
return buildOverride(rest, value);
|
|
72
|
+
}
|
|
73
|
+
return {
|
|
74
|
+
[head]: rest.length ? buildOverride(rest, value) : value,
|
|
75
|
+
};
|
|
76
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
import { ensureProjectConfig, loadProjectConfig } from '../core/config.js';
|
|
10
|
+
import { bootstrapEnvFile } from '../core/env.js';
|
|
11
|
+
import { detectPackageManager, installDependencies, normalizePackageManager, } from '../core/package-manager.js';
|
|
12
|
+
import { downloadAndExtractTemplate } from '../services/templates.js';
|
|
13
|
+
import { validateProjectName } from '../utils/validation.js';
|
|
14
|
+
import { runDev } from './dev.js';
|
|
15
|
+
const DEFAULT_TEMPLATE_REPO = 'hexastack/hexabot-template-starter';
|
|
16
|
+
export const registerCreateCommand = (program) => {
|
|
17
|
+
program
|
|
18
|
+
.command('create <projectName>')
|
|
19
|
+
.description('Create a new Hexabot project from the starter')
|
|
20
|
+
.option('-t, --template <name>', 'Project template to use (default: starter)')
|
|
21
|
+
.option('--pm <npm|pnpm|yarn|bun>', 'Preferred package manager')
|
|
22
|
+
.option('--no-install', 'Skip installing dependencies')
|
|
23
|
+
.option('--dev', 'Run hexabot dev after creation')
|
|
24
|
+
.option('--docker', 'Bootstrap Docker env files during creation')
|
|
25
|
+
.option('--force', 'Allow scaffolding into a non-empty directory')
|
|
26
|
+
.action(async (projectName, options) => {
|
|
27
|
+
await createProject(projectName, options);
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
const createProject = async (projectName, options) => {
|
|
31
|
+
if (!validateProjectName(projectName)) {
|
|
32
|
+
console.error(chalk.red('Invalid project name. Use lowercase letters, numbers, and dashes.'));
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
const projectPath = path.join(process.cwd(), projectName);
|
|
36
|
+
ensureTargetDirectory(projectPath, options.force);
|
|
37
|
+
const templateRepo = resolveTemplateRepo(options.template);
|
|
38
|
+
console.log(chalk.blue(`Using template ${templateRepo}`));
|
|
39
|
+
try {
|
|
40
|
+
const latestTag = await fetchLatestReleaseTag(templateRepo);
|
|
41
|
+
const templateUrl = `https://github.com/${templateRepo}/archive/refs/tags/${latestTag}.zip`;
|
|
42
|
+
await downloadAndExtractTemplate(templateUrl, projectPath);
|
|
43
|
+
const pmPreference = normalizePackageManager(options.pm);
|
|
44
|
+
const detectedPm = detectPackageManager(projectPath);
|
|
45
|
+
const packageManager = pmPreference || detectedPm;
|
|
46
|
+
const configOverrides = {
|
|
47
|
+
packageManager,
|
|
48
|
+
};
|
|
49
|
+
ensureProjectConfig(projectPath, configOverrides);
|
|
50
|
+
const config = loadProjectConfig(projectPath);
|
|
51
|
+
bootstrapEnvFile(projectPath, config.env.localExample, config.env.local, {
|
|
52
|
+
quiet: true,
|
|
53
|
+
});
|
|
54
|
+
if (options.docker) {
|
|
55
|
+
bootstrapEnvFile(projectPath, config.env.dockerExample, config.env.docker, { quiet: true });
|
|
56
|
+
}
|
|
57
|
+
if (options.noInstall) {
|
|
58
|
+
console.log(chalk.yellow('Skipping dependency installation (--no-install).'));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.log(chalk.blue(`Installing dependencies with ${packageManager}...`));
|
|
62
|
+
installDependencies(packageManager, projectPath);
|
|
63
|
+
}
|
|
64
|
+
logSuccessMessage(projectName, { docker: options.docker });
|
|
65
|
+
if (options.dev) {
|
|
66
|
+
if (options.noInstall) {
|
|
67
|
+
console.log(chalk.yellow('Dependencies were not installed. Run `npm install` before `--dev`.'));
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log(chalk.blue('Starting dev server...'));
|
|
71
|
+
await runDev({
|
|
72
|
+
cwd: projectPath,
|
|
73
|
+
docker: options.docker,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
console.error(chalk.red('Error creating project.'), error);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
};
|
|
83
|
+
const resolveTemplateRepo = (template) => {
|
|
84
|
+
if (!template) {
|
|
85
|
+
return DEFAULT_TEMPLATE_REPO;
|
|
86
|
+
}
|
|
87
|
+
if (template.includes('/')) {
|
|
88
|
+
return template;
|
|
89
|
+
}
|
|
90
|
+
return `hexastack/hexabot-template-${template}`;
|
|
91
|
+
};
|
|
92
|
+
const ensureTargetDirectory = (projectPath, force) => {
|
|
93
|
+
if (fs.existsSync(projectPath)) {
|
|
94
|
+
const isEmpty = fs.readdirSync(projectPath).length === 0;
|
|
95
|
+
if (!isEmpty && !force) {
|
|
96
|
+
console.error(chalk.red(`Directory ${projectPath} is not empty. Use --force to scaffold anyway.`));
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
else {
|
|
101
|
+
fs.mkdirSync(projectPath, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const fetchLatestReleaseTag = async (templateRepo) => {
|
|
105
|
+
const response = await fetch(`https://api.github.com/repos/${templateRepo}/releases/latest`);
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
if (!response.ok) {
|
|
108
|
+
throw new Error(`Failed to fetch the latest release information: ${data.message}`);
|
|
109
|
+
}
|
|
110
|
+
return data.tag_name;
|
|
111
|
+
};
|
|
112
|
+
const logSuccessMessage = (projectName, options) => {
|
|
113
|
+
console.log('\n');
|
|
114
|
+
console.log(chalk.green(`🎉 Project ${projectName} created successfully.`));
|
|
115
|
+
console.log('\n');
|
|
116
|
+
console.log(chalk.bgYellow.black(`Next steps:`));
|
|
117
|
+
console.log(chalk.gray(`1. Navigate to the project folder:`));
|
|
118
|
+
console.log(chalk.yellow(` cd ${projectName}`));
|
|
119
|
+
if (options.docker) {
|
|
120
|
+
console.log(chalk.gray(`2. Run dev mode with Docker (or omit --docker for local sqlite):`));
|
|
121
|
+
console.log(chalk.yellow(` hexabot dev --docker`));
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
console.log(chalk.gray(`2. Start local dev server (SQLite by default):`));
|
|
125
|
+
console.log(chalk.yellow(` hexabot dev`));
|
|
126
|
+
}
|
|
127
|
+
console.log(chalk.gray(`3. Explore docker helpers if needed:`));
|
|
128
|
+
console.log(chalk.yellow(` hexabot docker up --services postgres`));
|
|
129
|
+
console.log(chalk.gray(`Need env files? Run ${chalk.white('hexabot env init --docker')}`));
|
|
130
|
+
console.log('\n');
|
|
131
|
+
};
|