@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,168 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { jest } from '@jest/globals';
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
|
|
10
|
+
const loadProjectConfig = jest.fn();
|
|
11
|
+
const dockerCompose = jest.fn();
|
|
12
|
+
const generateComposeFiles = jest.fn();
|
|
13
|
+
const resolveComposeFile = jest.fn();
|
|
14
|
+
const bootstrapEnvFile = jest.fn();
|
|
15
|
+
const checkDocker = jest.fn();
|
|
16
|
+
const assertHexabotProject = jest.fn();
|
|
17
|
+
const ensureDockerFolder = jest.fn();
|
|
18
|
+
const parseServices = jest.fn();
|
|
19
|
+
const runStart = jest.fn();
|
|
20
|
+
|
|
21
|
+
jest.unstable_mockModule('../../core/config.js', () => ({
|
|
22
|
+
loadProjectConfig,
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
jest.unstable_mockModule('../../core/docker.js', () => ({
|
|
26
|
+
dockerCompose,
|
|
27
|
+
generateComposeFiles,
|
|
28
|
+
resolveComposeFile,
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
jest.unstable_mockModule('../../core/env.js', () => ({
|
|
32
|
+
bootstrapEnvFile,
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
jest.unstable_mockModule('../../core/prerequisites.js', () => ({
|
|
36
|
+
checkDocker,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
jest.unstable_mockModule('../../core/project.js', () => ({
|
|
40
|
+
assertHexabotProject,
|
|
41
|
+
ensureDockerFolder,
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
jest.unstable_mockModule('../../utils/services.js', () => ({
|
|
45
|
+
parseServices,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
jest.unstable_mockModule('../start.js', () => ({
|
|
49
|
+
runStart,
|
|
50
|
+
}));
|
|
51
|
+
|
|
52
|
+
let registerDockerCommand: (program: Command) => void;
|
|
53
|
+
|
|
54
|
+
beforeAll(async () => {
|
|
55
|
+
({ registerDockerCommand } = await import('../docker.js'));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
jest.resetAllMocks();
|
|
60
|
+
parseServices.mockImplementation((value: unknown) =>
|
|
61
|
+
(typeof value === 'string' ? value : '')
|
|
62
|
+
.split(',')
|
|
63
|
+
.filter((entry) => entry),
|
|
64
|
+
);
|
|
65
|
+
resolveComposeFile.mockReturnValue('/project/docker/docker-compose.yml');
|
|
66
|
+
generateComposeFiles.mockReturnValue('-f docker-compose.yml');
|
|
67
|
+
loadProjectConfig.mockReturnValue({
|
|
68
|
+
docker: {
|
|
69
|
+
composeFile: 'docker/docker-compose.yml',
|
|
70
|
+
defaultServices: ['api'],
|
|
71
|
+
},
|
|
72
|
+
env: {
|
|
73
|
+
docker: '.env.docker',
|
|
74
|
+
dockerExample: '.env.docker.example',
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const createProgram = () => {
|
|
80
|
+
const program = new Command();
|
|
81
|
+
registerDockerCommand(program);
|
|
82
|
+
|
|
83
|
+
return program;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
describe('registerDockerCommand', () => {
|
|
87
|
+
it('runs docker compose up with extra flags and services', async () => {
|
|
88
|
+
const program = createProgram();
|
|
89
|
+
await program.parseAsync([
|
|
90
|
+
'node',
|
|
91
|
+
'test',
|
|
92
|
+
'docker',
|
|
93
|
+
'up',
|
|
94
|
+
'--services',
|
|
95
|
+
'api,postgres',
|
|
96
|
+
'--build',
|
|
97
|
+
'--detach',
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
expect(bootstrapEnvFile).toHaveBeenCalled();
|
|
101
|
+
expect(generateComposeFiles).toHaveBeenCalledWith(
|
|
102
|
+
'/project/docker/docker-compose.yml',
|
|
103
|
+
['api', 'postgres'],
|
|
104
|
+
'dev',
|
|
105
|
+
);
|
|
106
|
+
expect(dockerCompose).toHaveBeenCalledWith(
|
|
107
|
+
expect.stringContaining('up --build -d'),
|
|
108
|
+
);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('runs docker compose down with optional volume removal', async () => {
|
|
112
|
+
const program = createProgram();
|
|
113
|
+
await program.parseAsync(['node', 'test', 'docker', 'down', '--volumes']);
|
|
114
|
+
|
|
115
|
+
expect(dockerCompose).toHaveBeenCalledWith(
|
|
116
|
+
expect.stringContaining('down -v'),
|
|
117
|
+
);
|
|
118
|
+
expect(bootstrapEnvFile).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('tails docker logs with filters', async () => {
|
|
122
|
+
const program = createProgram();
|
|
123
|
+
await program.parseAsync([
|
|
124
|
+
'node',
|
|
125
|
+
'test',
|
|
126
|
+
'docker',
|
|
127
|
+
'logs',
|
|
128
|
+
'api',
|
|
129
|
+
'--follow',
|
|
130
|
+
'--since',
|
|
131
|
+
'1h',
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
expect(dockerCompose).toHaveBeenCalledWith(
|
|
135
|
+
expect.stringContaining('logs -f --since 1h api'),
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('lists running services with docker ps', async () => {
|
|
140
|
+
const program = createProgram();
|
|
141
|
+
await program.parseAsync(['node', 'test', 'docker', 'ps']);
|
|
142
|
+
|
|
143
|
+
expect(dockerCompose).toHaveBeenCalledWith(expect.stringContaining('ps'));
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('delegates to start command for docker start', async () => {
|
|
147
|
+
const program = createProgram();
|
|
148
|
+
await program.parseAsync([
|
|
149
|
+
'node',
|
|
150
|
+
'test',
|
|
151
|
+
'docker',
|
|
152
|
+
'start',
|
|
153
|
+
'--services',
|
|
154
|
+
'api',
|
|
155
|
+
'--build',
|
|
156
|
+
'--detach',
|
|
157
|
+
'--env-bootstrap',
|
|
158
|
+
]);
|
|
159
|
+
|
|
160
|
+
expect(runStart).toHaveBeenCalledWith({
|
|
161
|
+
docker: true,
|
|
162
|
+
services: 'api',
|
|
163
|
+
build: true,
|
|
164
|
+
detach: true,
|
|
165
|
+
envBootstrap: true,
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { jest } from '@jest/globals';
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
|
|
10
|
+
const loadProjectConfig = jest.fn();
|
|
11
|
+
const bootstrapEnvFile = jest.fn();
|
|
12
|
+
const listEnvStatus = jest.fn();
|
|
13
|
+
const assertHexabotProject = jest.fn();
|
|
14
|
+
|
|
15
|
+
jest.unstable_mockModule('../../core/config.js', () => ({
|
|
16
|
+
loadProjectConfig,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
jest.unstable_mockModule('../../core/env.js', () => ({
|
|
20
|
+
bootstrapEnvFile,
|
|
21
|
+
listEnvStatus,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
jest.unstable_mockModule('../../core/project.js', () => ({
|
|
25
|
+
assertHexabotProject,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
let registerEnvCommand: (program: Command) => void;
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
({ registerEnvCommand } = await import('../env.js'));
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
jest.resetAllMocks();
|
|
36
|
+
loadProjectConfig.mockReturnValue({
|
|
37
|
+
env: {
|
|
38
|
+
local: '.env',
|
|
39
|
+
localExample: '.env.example',
|
|
40
|
+
docker: '.env.docker',
|
|
41
|
+
dockerExample: '.env.docker.example',
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
describe('registerEnvCommand', () => {
|
|
47
|
+
it('bootstraps env files for docker or local targets', async () => {
|
|
48
|
+
const dockerProgram = new Command();
|
|
49
|
+
registerEnvCommand(dockerProgram);
|
|
50
|
+
await dockerProgram.parseAsync([
|
|
51
|
+
'node',
|
|
52
|
+
'test',
|
|
53
|
+
'env',
|
|
54
|
+
'init',
|
|
55
|
+
'--docker',
|
|
56
|
+
'--force',
|
|
57
|
+
]);
|
|
58
|
+
expect(bootstrapEnvFile).toHaveBeenCalledWith(
|
|
59
|
+
expect.any(String),
|
|
60
|
+
'.env.docker.example',
|
|
61
|
+
'.env.docker',
|
|
62
|
+
{ force: true },
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
jest.clearAllMocks();
|
|
66
|
+
const localProgram = new Command();
|
|
67
|
+
registerEnvCommand(localProgram);
|
|
68
|
+
await localProgram.parseAsync(['node', 'test', 'env', 'init']);
|
|
69
|
+
expect(bootstrapEnvFile).toHaveBeenCalledWith(
|
|
70
|
+
expect.any(String),
|
|
71
|
+
'.env.example',
|
|
72
|
+
'.env',
|
|
73
|
+
{ force: undefined },
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('lists env statuses with formatted output', async () => {
|
|
78
|
+
const statuses = [
|
|
79
|
+
{ file: '.env', exists: true },
|
|
80
|
+
{ file: '.env.docker', exists: false },
|
|
81
|
+
];
|
|
82
|
+
listEnvStatus.mockReturnValue(statuses);
|
|
83
|
+
const logs: string[] = [];
|
|
84
|
+
jest.spyOn(console, 'log').mockImplementation((message?: string) => {
|
|
85
|
+
logs.push(String(message));
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const program = new Command();
|
|
89
|
+
registerEnvCommand(program);
|
|
90
|
+
await program.parseAsync(['node', 'test', 'env', 'list']);
|
|
91
|
+
|
|
92
|
+
expect(logs.join('\n')).toContain('.env');
|
|
93
|
+
expect(logs.join('\n')).toContain('.env.docker');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { jest } from '@jest/globals';
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
|
|
10
|
+
const dockerExec = jest.fn();
|
|
11
|
+
const ensureDockerFolder = jest.fn();
|
|
12
|
+
const checkDocker = jest.fn();
|
|
13
|
+
|
|
14
|
+
jest.unstable_mockModule('../../core/docker.js', () => ({
|
|
15
|
+
dockerExec,
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
jest.unstable_mockModule('../../core/project.js', () => ({
|
|
19
|
+
ensureDockerFolder,
|
|
20
|
+
}));
|
|
21
|
+
|
|
22
|
+
jest.unstable_mockModule('../../core/prerequisites.js', () => ({
|
|
23
|
+
checkDocker,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
let registerMigrateCommand: (program: Command) => void;
|
|
27
|
+
|
|
28
|
+
beforeAll(async () => {
|
|
29
|
+
({ registerMigrateCommand } = await import('../migrate.js'));
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('registerMigrateCommand', () => {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
jest.clearAllMocks();
|
|
35
|
+
ensureDockerFolder.mockReturnValue('/workspace/docker');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('runs migrations with additional arguments', async () => {
|
|
39
|
+
const program = new Command();
|
|
40
|
+
registerMigrateCommand(program);
|
|
41
|
+
|
|
42
|
+
await program.parseAsync(['node', 'test', 'migrate', 'seed', '20241128']);
|
|
43
|
+
|
|
44
|
+
expect(ensureDockerFolder).toHaveBeenCalled();
|
|
45
|
+
expect(dockerExec).toHaveBeenCalledWith(
|
|
46
|
+
'api',
|
|
47
|
+
'npm run migrate seed 20241128',
|
|
48
|
+
'--user $(id -u):$(id -g)',
|
|
49
|
+
);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('runs migrations without extra arguments', async () => {
|
|
53
|
+
const program = new Command();
|
|
54
|
+
registerMigrateCommand(program);
|
|
55
|
+
|
|
56
|
+
await program.parseAsync(['node', 'test', 'migrate']);
|
|
57
|
+
|
|
58
|
+
expect(dockerExec).toHaveBeenCalledWith(
|
|
59
|
+
'api',
|
|
60
|
+
'npm run migrate',
|
|
61
|
+
'--user $(id -u):$(id -g)',
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { jest } from '@jest/globals';
|
|
8
|
+
|
|
9
|
+
const loadProjectConfig = jest.fn();
|
|
10
|
+
const updateProjectConfig = jest.fn();
|
|
11
|
+
const dockerCompose = jest.fn();
|
|
12
|
+
const generateComposeFiles = jest.fn();
|
|
13
|
+
const resolveComposeFile = jest.fn();
|
|
14
|
+
const bootstrapEnvFile = jest.fn();
|
|
15
|
+
const resolveEnvExample = jest.fn();
|
|
16
|
+
const detectPackageManager = jest.fn();
|
|
17
|
+
const normalizePackageManager = jest.fn();
|
|
18
|
+
const runPackageScript = jest.fn();
|
|
19
|
+
const checkDocker = jest.fn();
|
|
20
|
+
const assertHexabotProject = jest.fn();
|
|
21
|
+
const ensureDockerFolder = jest.fn();
|
|
22
|
+
|
|
23
|
+
jest.unstable_mockModule('../../core/config.js', () => ({
|
|
24
|
+
loadProjectConfig,
|
|
25
|
+
updateProjectConfig,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.unstable_mockModule('../../core/docker.js', () => ({
|
|
29
|
+
dockerCompose,
|
|
30
|
+
generateComposeFiles,
|
|
31
|
+
resolveComposeFile,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
jest.unstable_mockModule('../../core/env.js', () => ({
|
|
35
|
+
bootstrapEnvFile,
|
|
36
|
+
resolveEnvExample,
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
jest.unstable_mockModule('../../core/package-manager.js', () => ({
|
|
40
|
+
detectPackageManager,
|
|
41
|
+
normalizePackageManager,
|
|
42
|
+
runPackageScript,
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
jest.unstable_mockModule('../../core/prerequisites.js', () => ({
|
|
46
|
+
checkDocker,
|
|
47
|
+
}));
|
|
48
|
+
|
|
49
|
+
jest.unstable_mockModule('../../core/project.js', () => ({
|
|
50
|
+
assertHexabotProject,
|
|
51
|
+
ensureDockerFolder,
|
|
52
|
+
}));
|
|
53
|
+
|
|
54
|
+
jest.unstable_mockModule('../../utils/services.js', () => ({
|
|
55
|
+
parseServices: (value: string) => value.split(',').filter(Boolean),
|
|
56
|
+
}));
|
|
57
|
+
|
|
58
|
+
let runStart: (options?: any) => Promise<void>;
|
|
59
|
+
|
|
60
|
+
beforeAll(async () => {
|
|
61
|
+
({ runStart } = await import('../start.js'));
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
beforeEach(() => {
|
|
65
|
+
jest.resetAllMocks();
|
|
66
|
+
normalizePackageManager.mockImplementation(
|
|
67
|
+
(value: unknown) => value as string | undefined,
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('runStart', () => {
|
|
72
|
+
const baseConfig = {
|
|
73
|
+
startScript: 'start',
|
|
74
|
+
packageManager: 'npm',
|
|
75
|
+
docker: {
|
|
76
|
+
composeFile: 'docker/docker-compose.yml',
|
|
77
|
+
defaultServices: [],
|
|
78
|
+
},
|
|
79
|
+
env: {
|
|
80
|
+
local: '.env',
|
|
81
|
+
localExample: '.env.example',
|
|
82
|
+
docker: '.env.docker',
|
|
83
|
+
dockerExample: '.env.docker.example',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
it('runs the npm start script when not using Docker', async () => {
|
|
88
|
+
loadProjectConfig.mockReturnValue(baseConfig);
|
|
89
|
+
|
|
90
|
+
await runStart();
|
|
91
|
+
|
|
92
|
+
expect(assertHexabotProject).toHaveBeenCalled();
|
|
93
|
+
expect(runPackageScript).toHaveBeenCalledWith(
|
|
94
|
+
'npm',
|
|
95
|
+
'start',
|
|
96
|
+
expect.any(String),
|
|
97
|
+
);
|
|
98
|
+
expect(detectPackageManager).not.toHaveBeenCalled();
|
|
99
|
+
expect(updateProjectConfig).not.toHaveBeenCalled();
|
|
100
|
+
expect(dockerCompose).not.toHaveBeenCalled();
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('runs docker compose in prod mode with env bootstrap', async () => {
|
|
104
|
+
const config = {
|
|
105
|
+
...baseConfig,
|
|
106
|
+
packageManager: undefined,
|
|
107
|
+
docker: {
|
|
108
|
+
composeFile: 'docker/docker-compose.yml',
|
|
109
|
+
defaultServices: ['api'],
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
loadProjectConfig.mockReturnValue(config);
|
|
113
|
+
detectPackageManager.mockReturnValue('pnpm');
|
|
114
|
+
resolveComposeFile.mockReturnValue('/tmp/docker/docker-compose.yml');
|
|
115
|
+
generateComposeFiles.mockReturnValue('-f docker-compose.yml');
|
|
116
|
+
|
|
117
|
+
await runStart({
|
|
118
|
+
docker: true,
|
|
119
|
+
services: 'api,postgres',
|
|
120
|
+
envBootstrap: true,
|
|
121
|
+
build: true,
|
|
122
|
+
detach: true,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
expect(detectPackageManager).toHaveBeenCalled();
|
|
126
|
+
expect(updateProjectConfig).toHaveBeenCalledWith(expect.any(String), {
|
|
127
|
+
packageManager: 'pnpm',
|
|
128
|
+
});
|
|
129
|
+
expect(bootstrapEnvFile).toHaveBeenCalledWith(
|
|
130
|
+
expect.any(String),
|
|
131
|
+
config.env.dockerExample,
|
|
132
|
+
config.env.docker,
|
|
133
|
+
);
|
|
134
|
+
expect(resolveEnvExample).not.toHaveBeenCalled();
|
|
135
|
+
expect(checkDocker).toHaveBeenCalled();
|
|
136
|
+
expect(ensureDockerFolder).toHaveBeenCalled();
|
|
137
|
+
expect(generateComposeFiles).toHaveBeenCalledWith(
|
|
138
|
+
'/tmp/docker/docker-compose.yml',
|
|
139
|
+
['api', 'postgres'],
|
|
140
|
+
'prod',
|
|
141
|
+
);
|
|
142
|
+
expect(dockerCompose).toHaveBeenCalledWith(
|
|
143
|
+
expect.stringContaining('up --build -d'),
|
|
144
|
+
);
|
|
145
|
+
expect(runPackageScript).not.toHaveBeenCalled();
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('bootstraps envs for local start when requested', async () => {
|
|
149
|
+
loadProjectConfig.mockReturnValue(baseConfig);
|
|
150
|
+
resolveEnvExample.mockReturnValue('.env.local.example');
|
|
151
|
+
|
|
152
|
+
await runStart({ envBootstrap: true, env: '.env.local' });
|
|
153
|
+
|
|
154
|
+
expect(resolveEnvExample).toHaveBeenCalledWith(
|
|
155
|
+
expect.any(String),
|
|
156
|
+
'.env.local',
|
|
157
|
+
baseConfig.env.localExample,
|
|
158
|
+
);
|
|
159
|
+
expect(bootstrapEnvFile).toHaveBeenCalledWith(
|
|
160
|
+
expect.any(String),
|
|
161
|
+
'.env.local.example',
|
|
162
|
+
'.env.local',
|
|
163
|
+
);
|
|
164
|
+
expect(runPackageScript).toHaveBeenCalled();
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { Command } from 'commander';
|
|
9
|
+
|
|
10
|
+
import { loadProjectConfig } from '../core/config.js';
|
|
11
|
+
import { listEnvStatus } from '../core/env.js';
|
|
12
|
+
import { checkDocker, checkNodeVersion } from '../core/prerequisites.js';
|
|
13
|
+
import { isHexabotProject } from '../core/project.js';
|
|
14
|
+
|
|
15
|
+
interface CheckCommandOptions {
|
|
16
|
+
dockerOnly?: boolean;
|
|
17
|
+
docker?: boolean;
|
|
18
|
+
noDocker?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const registerCheckCommand = (program: Command) => {
|
|
22
|
+
program
|
|
23
|
+
.command('check')
|
|
24
|
+
.description('Run diagnostic checks')
|
|
25
|
+
.option('--docker-only', 'Only run Docker checks')
|
|
26
|
+
.option('--no-docker', 'Skip Docker checks')
|
|
27
|
+
.action((options: CheckCommandOptions) => {
|
|
28
|
+
runDiagnostics({
|
|
29
|
+
dockerOnly: options.dockerOnly,
|
|
30
|
+
noDocker: options.noDocker ?? options.docker === false,
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const runDiagnostics = (options: {
|
|
36
|
+
dockerOnly?: boolean;
|
|
37
|
+
noDocker?: boolean;
|
|
38
|
+
}) => {
|
|
39
|
+
const projectRoot = process.cwd();
|
|
40
|
+
const onlyDocker = options.dockerOnly;
|
|
41
|
+
const skipDocker = options.noDocker;
|
|
42
|
+
const results: DiagnosticResult[] = [];
|
|
43
|
+
|
|
44
|
+
if (!onlyDocker) {
|
|
45
|
+
const nodeResult = checkNodeVersion({ fatal: false, silent: true });
|
|
46
|
+
results.push({
|
|
47
|
+
label: 'Node.js version',
|
|
48
|
+
ok: nodeResult.ok,
|
|
49
|
+
message: nodeResult.message,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const projectResult = isHexabotProject(projectRoot);
|
|
53
|
+
results.push({
|
|
54
|
+
label: 'Hexabot project',
|
|
55
|
+
ok: projectResult,
|
|
56
|
+
message: projectResult
|
|
57
|
+
? 'Found @hexabot-ai/api in package.json'
|
|
58
|
+
: 'Run inside a Hexabot project directory',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
if (projectResult) {
|
|
62
|
+
const config = loadProjectConfig(projectRoot);
|
|
63
|
+
const envStatuses = listEnvStatus(projectRoot, config);
|
|
64
|
+
envStatuses.forEach((status) => {
|
|
65
|
+
results.push({
|
|
66
|
+
label: `Env file ${status.file}`,
|
|
67
|
+
ok: status.exists,
|
|
68
|
+
message: status.exists ? 'Found' : 'Missing',
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!skipDocker) {
|
|
75
|
+
const dockerResult = checkDocker({ fatal: false, silent: true });
|
|
76
|
+
results.push({
|
|
77
|
+
label: 'Docker',
|
|
78
|
+
ok: dockerResult.ok,
|
|
79
|
+
message: dockerResult.message,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const hasErrors = results.some((result) => !result.ok);
|
|
84
|
+
results.forEach((result) => printResult(result));
|
|
85
|
+
|
|
86
|
+
if (hasErrors) {
|
|
87
|
+
process.exitCode = 1;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
interface DiagnosticResult {
|
|
92
|
+
label: string;
|
|
93
|
+
ok: boolean;
|
|
94
|
+
message?: string;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const printResult = (result: DiagnosticResult) => {
|
|
98
|
+
const status = result.ok ? chalk.green('PASS') : chalk.red('FAIL');
|
|
99
|
+
console.log(
|
|
100
|
+
`${status} ${result.label}${result.message ? ` — ${result.message}` : ''}`,
|
|
101
|
+
);
|
|
102
|
+
};
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { Command } from 'commander';
|
|
11
|
+
|
|
12
|
+
import { loadProjectConfig, updateProjectConfig } from '../core/config.js';
|
|
13
|
+
import { normalizePackageManager } from '../core/package-manager.js';
|
|
14
|
+
import { assertHexabotProject } from '../core/project.js';
|
|
15
|
+
|
|
16
|
+
export const registerConfigCommand = (program: Command) => {
|
|
17
|
+
const configCommand = program.command('config').description('Project config');
|
|
18
|
+
|
|
19
|
+
configCommand
|
|
20
|
+
.command('show')
|
|
21
|
+
.description('Print effective Hexabot configuration')
|
|
22
|
+
.action(() => {
|
|
23
|
+
const projectRoot = path.resolve(process.cwd());
|
|
24
|
+
assertHexabotProject(projectRoot);
|
|
25
|
+
const config = loadProjectConfig(projectRoot);
|
|
26
|
+
console.log(JSON.stringify(config, null, 2));
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
configCommand
|
|
30
|
+
.command('set <key> <value>')
|
|
31
|
+
.description('Update a configuration value (dot notation)')
|
|
32
|
+
.action((key: string, value: string) => {
|
|
33
|
+
const projectRoot = path.resolve(process.cwd());
|
|
34
|
+
assertHexabotProject(projectRoot);
|
|
35
|
+
const normalizedValue = parseValue(key, value);
|
|
36
|
+
const override = buildOverride(key.split('.'), normalizedValue);
|
|
37
|
+
const updated = updateProjectConfig(projectRoot, override);
|
|
38
|
+
console.log(chalk.green('Updated hexabot.config.json'));
|
|
39
|
+
console.log(JSON.stringify(updated, null, 2));
|
|
40
|
+
});
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const parseValue = (key: string, value: string) => {
|
|
44
|
+
if (key === 'packageManager') {
|
|
45
|
+
return normalizePackageManager(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const trimmed = value.trim();
|
|
49
|
+
if (trimmed === 'true') {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
if (trimmed === 'false') {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
if (!Number.isNaN(Number(trimmed)) && trimmed !== '') {
|
|
56
|
+
return Number(trimmed);
|
|
57
|
+
}
|
|
58
|
+
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(trimmed);
|
|
61
|
+
} catch (_error) {
|
|
62
|
+
// fallback to string
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if (trimmed.includes(',') && key.endsWith('Services')) {
|
|
66
|
+
return trimmed
|
|
67
|
+
.split(',')
|
|
68
|
+
.map((entry) => entry.trim())
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return trimmed;
|
|
73
|
+
};
|
|
74
|
+
const buildOverride = (
|
|
75
|
+
parts: string[],
|
|
76
|
+
value: unknown,
|
|
77
|
+
): Record<string, unknown> => {
|
|
78
|
+
if (!parts.length) {
|
|
79
|
+
return {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const [head, ...rest] = parts;
|
|
83
|
+
if (!head) {
|
|
84
|
+
return buildOverride(rest, value);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
[head]: rest.length ? buildOverride(rest, value) : value,
|
|
89
|
+
};
|
|
90
|
+
};
|