@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.
Files changed (81) hide show
  1. package/.prettierrc +5 -0
  2. package/AGENTS.md +64 -0
  3. package/README.md +192 -0
  4. package/dist/cli.js +31 -0
  5. package/dist/commands/__tests__/check.test.js +97 -0
  6. package/dist/commands/__tests__/config.test.js +80 -0
  7. package/dist/commands/__tests__/dev.test.js +105 -0
  8. package/dist/commands/__tests__/docker.test.js +132 -0
  9. package/dist/commands/__tests__/env.test.js +72 -0
  10. package/dist/commands/__tests__/migrate.test.js +42 -0
  11. package/dist/commands/__tests__/start.test.js +120 -0
  12. package/dist/commands/check.js +73 -0
  13. package/dist/commands/config.js +76 -0
  14. package/dist/commands/create.js +131 -0
  15. package/dist/commands/dev.js +72 -0
  16. package/dist/commands/docker.js +119 -0
  17. package/dist/commands/env.js +44 -0
  18. package/dist/commands/migrate.js +22 -0
  19. package/dist/commands/start.js +76 -0
  20. package/dist/core/__tests__/config.test.js +88 -0
  21. package/dist/core/__tests__/docker.test.js +43 -0
  22. package/dist/core/__tests__/env.test.js +71 -0
  23. package/dist/core/__tests__/package-manager.test.js +95 -0
  24. package/dist/core/__tests__/project.test.js +49 -0
  25. package/dist/core/config.js +78 -0
  26. package/dist/core/docker.js +66 -0
  27. package/dist/core/env.js +50 -0
  28. package/dist/core/package-manager.js +87 -0
  29. package/dist/core/prerequisites.js +80 -0
  30. package/dist/core/project.js +58 -0
  31. package/dist/index.js +16 -0
  32. package/dist/services/templates.js +27 -0
  33. package/dist/ui/banner.js +14 -0
  34. package/dist/utils/__tests__/services.test.js +18 -0
  35. package/dist/utils/__tests__/validation.test.js +17 -0
  36. package/dist/utils/__tests__/version.test.js +27 -0
  37. package/dist/utils/services.js +11 -0
  38. package/dist/utils/validation.js +9 -0
  39. package/dist/utils/version.js +22 -0
  40. package/eslint.config-staged.cjs +10 -0
  41. package/eslint.config.cjs +104 -0
  42. package/jest.config.ts +24 -0
  43. package/package.json +63 -0
  44. package/src/cli.ts +37 -0
  45. package/src/commands/__tests__/check.test.ts +116 -0
  46. package/src/commands/__tests__/config.test.ts +97 -0
  47. package/src/commands/__tests__/dev.test.ts +151 -0
  48. package/src/commands/__tests__/docker.test.ts +168 -0
  49. package/src/commands/__tests__/env.test.ts +95 -0
  50. package/src/commands/__tests__/migrate.test.ts +64 -0
  51. package/src/commands/__tests__/start.test.ts +166 -0
  52. package/src/commands/check.ts +102 -0
  53. package/src/commands/config.ts +90 -0
  54. package/src/commands/create.ts +201 -0
  55. package/src/commands/dev.ts +122 -0
  56. package/src/commands/docker.ts +190 -0
  57. package/src/commands/env.ts +62 -0
  58. package/src/commands/migrate.ts +27 -0
  59. package/src/commands/start.ts +126 -0
  60. package/src/core/__tests__/config.test.ts +114 -0
  61. package/src/core/__tests__/docker.test.ts +59 -0
  62. package/src/core/__tests__/env.test.ts +97 -0
  63. package/src/core/__tests__/package-manager.test.ts +121 -0
  64. package/src/core/__tests__/project.test.ts +68 -0
  65. package/src/core/config.ts +127 -0
  66. package/src/core/docker.ts +91 -0
  67. package/src/core/env.ts +90 -0
  68. package/src/core/package-manager.ts +126 -0
  69. package/src/core/prerequisites.ts +117 -0
  70. package/src/core/project.ts +97 -0
  71. package/src/index.ts +21 -0
  72. package/src/services/templates.ts +33 -0
  73. package/src/ui/banner.ts +18 -0
  74. package/src/utils/__tests__/services.test.ts +21 -0
  75. package/src/utils/__tests__/validation.test.ts +21 -0
  76. package/src/utils/__tests__/version.test.ts +35 -0
  77. package/src/utils/services.ts +12 -0
  78. package/src/utils/validation.ts +11 -0
  79. package/src/utils/version.ts +28 -0
  80. package/test/__mocks__/chalk.ts +13 -0
  81. package/tsconfig.json +15 -0
@@ -0,0 +1,104 @@
1
+ const path = require('node:path');
2
+ const { FlatCompat } = require('@eslint/eslintrc');
3
+ const globals = require('globals');
4
+ const tsParser = require('@typescript-eslint/parser');
5
+ const tsPlugin = require('@typescript-eslint/eslint-plugin');
6
+ const importPlugin = require('eslint-plugin-import');
7
+ const headerPlugin = require('eslint-plugin-header');
8
+
9
+ if (!headerPlugin.rules.header.meta.schema) {
10
+ headerPlugin.rules.header.meta.schema = {
11
+ type: 'array',
12
+ };
13
+ }
14
+
15
+ const compat = new FlatCompat({
16
+ baseDirectory: __dirname,
17
+ resolvePluginsRelativeTo: __dirname,
18
+ });
19
+
20
+ const createConfig = ({ headerYear = '2025' } = {}) => {
21
+ const headerLines = [
22
+ '',
23
+ ' * Hexabot — Fair Core License (FCL-1.0-ALv2)',
24
+ {
25
+ pattern: '^ \\* Copyright \\(c\\) 20\\d{2} Hexastack\\.$',
26
+ template: ` * Copyright (c) ${headerYear} Hexastack.`,
27
+ },
28
+ ' * Full terms: see LICENSE.md.',
29
+ ' ',
30
+ ];
31
+
32
+ return [
33
+ {
34
+ ignores: ['dist', 'eslint.config.cjs', 'eslint.config-staged.cjs'],
35
+ },
36
+ ...compat.extends('plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'),
37
+ {
38
+ files: ['**/*.ts'],
39
+ languageOptions: {
40
+ parser: tsParser,
41
+ parserOptions: {
42
+ project: path.join(__dirname, 'tsconfig.json'),
43
+ tsconfigRootDir: __dirname,
44
+ sourceType: 'module',
45
+ },
46
+ globals: {
47
+ ...globals.node,
48
+ fetch: 'readonly',
49
+ },
50
+ },
51
+ plugins: {
52
+ '@typescript-eslint': tsPlugin,
53
+ import: importPlugin,
54
+ header: headerPlugin,
55
+ },
56
+ rules: {
57
+ '@typescript-eslint/explicit-function-return-type': 'off',
58
+ '@typescript-eslint/explicit-module-boundary-types': 'off',
59
+ '@typescript-eslint/no-explicit-any': 'off',
60
+ '@typescript-eslint/no-this-alias': 'off',
61
+ '@typescript-eslint/no-empty-object-type': 'off',
62
+ '@typescript-eslint/no-unused-vars': [
63
+ 'error',
64
+ {
65
+ argsIgnorePattern: '^_',
66
+ varsIgnorePattern: '^_',
67
+ caughtErrorsIgnorePattern: '^_',
68
+ },
69
+ ],
70
+ '@typescript-eslint/no-namespace': 'off',
71
+ 'padding-line-between-statements': [
72
+ 2,
73
+ { blankLine: 'always', prev: '*', next: 'export' },
74
+ { blankLine: 'always', prev: '*', next: 'function' },
75
+ { blankLine: 'always', prev: '*', next: 'return' },
76
+ { blankLine: 'never', prev: 'const', next: 'const' },
77
+ ],
78
+ 'lines-between-class-members': ['warn', 'always'],
79
+ 'no-console': 'off',
80
+ 'no-duplicate-imports': 2,
81
+ 'object-shorthand': 1,
82
+ 'import/order': [
83
+ 'error',
84
+ {
85
+ groups: ['builtin', 'external', 'unknown', 'parent', 'sibling', 'index', 'internal'],
86
+ 'newlines-between': 'always',
87
+ alphabetize: {
88
+ order: 'asc',
89
+ caseInsensitive: true,
90
+ },
91
+ },
92
+ ],
93
+ 'header/header': [2, 'block', headerLines, 2],
94
+ 'no-multiple-empty-lines': ['error', { max: 1 }],
95
+ },
96
+ },
97
+ ];
98
+ };
99
+
100
+ const config = createConfig();
101
+
102
+ config.createConfig = createConfig;
103
+
104
+ module.exports = config;
package/jest.config.ts ADDED
@@ -0,0 +1,24 @@
1
+ import type { Config } from 'jest';
2
+
3
+ const config: Config = {
4
+ preset: 'ts-jest/presets/default-esm',
5
+ testEnvironment: 'node',
6
+ extensionsToTreatAsEsm: ['.ts'],
7
+ transform: {
8
+ '^.+\\.(ts|tsx)$': [
9
+ 'ts-jest',
10
+ {
11
+ tsconfig: '<rootDir>/tsconfig.json',
12
+ useESM: true,
13
+ },
14
+ ],
15
+ },
16
+ moduleNameMapper: {
17
+ '^(\\.{1,2}/.*)\\.js$': '$1',
18
+ '^chalk$': '<rootDir>/test/__mocks__/chalk.ts',
19
+ },
20
+ testPathIgnorePatterns: ['/node_modules/', '/dist/'],
21
+ collectCoverageFrom: ['src/**/*.ts', '!src/index.ts'],
22
+ };
23
+
24
+ export default config;
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@hexabot-ai/cli",
3
+ "version": "3.0.0-alpha.3",
4
+ "description": "Official Hexabot CLI for creating and managing AI chatbot/agent projects built with Hexabot.",
5
+ "main": "dist/index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "hexabot": "dist/index.js"
9
+ },
10
+ "engines": {
11
+ "node": ">=20.18.1"
12
+ },
13
+ "scripts": {
14
+ "build": "tsc",
15
+ "typecheck": "tsc --noEmit",
16
+ "start": "node dist/index.js",
17
+ "dev": "tsx watch src/index.ts",
18
+ "test": "cross-env NODE_OPTIONS=--experimental-vm-modules jest",
19
+ "prepare": "npm run build",
20
+ "release:patch": "npm version patch && git push origin main --tags",
21
+ "release:minor": "npm version minor && git push origin main --tags",
22
+ "lint": "eslint \"src/**/*.ts\"",
23
+ "lint:fix": "eslint \"src/**/*.ts\" --fix"
24
+ },
25
+ "keywords": [],
26
+ "author": "Hexastack",
27
+ "license": "AGPL-3.0-only",
28
+ "lint-staged": {
29
+ "*.{ts}": "eslint --fix --config eslint.config-staged.cjs"
30
+ },
31
+ "dependencies": {
32
+ "axios": "^1.7.7",
33
+ "chalk": "^5.3.0",
34
+ "commander": "^12.1.0",
35
+ "decompress": "^4.2.1",
36
+ "degit": "^2.8.4",
37
+ "dotenv": "^16.4.5",
38
+ "figlet": "^1.7.0"
39
+ },
40
+ "devDependencies": {
41
+ "@types/chalk": "^2.2.0",
42
+ "@types/commander": "^2.12.2",
43
+ "@types/decompress": "^4.2.7",
44
+ "@types/degit": "^2.8.6",
45
+ "@types/figlet": "^1.5.8",
46
+ "@types/jest": "^30.0.0",
47
+ "@types/node": "^22.7.4",
48
+ "@typescript-eslint/eslint-plugin": "^8.46.0",
49
+ "@typescript-eslint/parser": "^8.46.0",
50
+ "cross-env": "^10.1.0",
51
+ "eslint": "^9.37.0",
52
+ "eslint-config-prettier": "^10.1.8",
53
+ "eslint-plugin-header": "^3.1.1",
54
+ "eslint-plugin-import": "^2.32.0",
55
+ "eslint-plugin-prettier": "^5.5.4",
56
+ "jest": "^30.2.0",
57
+ "lint-staged": "^15.3.0",
58
+ "prettier": "^3.6.2",
59
+ "ts-jest": "^29.4.5",
60
+ "tsx": "^4.19.2",
61
+ "typescript": "^5.6.2"
62
+ }
63
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,37 @@
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 { Command } from 'commander';
8
+
9
+ import { registerCheckCommand } from './commands/check.js';
10
+ import { registerConfigCommand } from './commands/config.js';
11
+ import { registerCreateCommand } from './commands/create.js';
12
+ import { registerDevCommand } from './commands/dev.js';
13
+ import { registerDockerCommand } from './commands/docker.js';
14
+ import { registerEnvCommand } from './commands/env.js';
15
+ import { registerMigrateCommand } from './commands/migrate.js';
16
+ import { registerStartCommand } from './commands/start.js';
17
+ import { getCliVersion } from './utils/version.js';
18
+
19
+ export const createCliProgram = () => {
20
+ const program = new Command();
21
+
22
+ program
23
+ .name('Hexabot')
24
+ .description('A CLI to manage your Hexabot project instance')
25
+ .version(getCliVersion());
26
+
27
+ registerCheckCommand(program);
28
+ registerCreateCommand(program);
29
+ registerConfigCommand(program);
30
+ registerDevCommand(program);
31
+ registerDockerCommand(program);
32
+ registerEnvCommand(program);
33
+ registerStartCommand(program);
34
+ registerMigrateCommand(program);
35
+
36
+ return program;
37
+ };
@@ -0,0 +1,116 @@
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 listEnvStatus = jest.fn();
12
+ const checkDocker = jest.fn();
13
+ const checkNodeVersion = jest.fn();
14
+ const isHexabotProject = jest.fn();
15
+
16
+ jest.unstable_mockModule('../../core/config.js', () => ({
17
+ loadProjectConfig,
18
+ }));
19
+
20
+ jest.unstable_mockModule('../../core/env.js', () => ({
21
+ listEnvStatus,
22
+ }));
23
+
24
+ jest.unstable_mockModule('../../core/prerequisites.js', () => ({
25
+ checkDocker,
26
+ checkNodeVersion,
27
+ }));
28
+
29
+ jest.unstable_mockModule('../../core/project.js', () => ({
30
+ isHexabotProject,
31
+ }));
32
+
33
+ let registerCheckCommand: (program: Command) => void;
34
+
35
+ beforeAll(async () => {
36
+ ({ registerCheckCommand } = await import('../check.js'));
37
+ });
38
+
39
+ beforeEach(() => {
40
+ jest.resetAllMocks();
41
+ process.exitCode = undefined;
42
+ });
43
+
44
+ describe('registerCheckCommand', () => {
45
+ it('runs full diagnostics by default', async () => {
46
+ checkNodeVersion.mockReturnValue({ ok: true, message: 'Node OK' });
47
+ isHexabotProject.mockReturnValue(true);
48
+ loadProjectConfig.mockReturnValue({
49
+ env: {
50
+ local: '.env',
51
+ },
52
+ });
53
+ listEnvStatus.mockReturnValue([
54
+ { file: '.env', exists: true },
55
+ { file: '.env.docker', exists: true },
56
+ ]);
57
+ checkDocker.mockReturnValue({ ok: true, message: 'Docker OK' });
58
+ const logs: string[] = [];
59
+ jest.spyOn(console, 'log').mockImplementation((message?: string) => {
60
+ logs.push(String(message));
61
+ });
62
+
63
+ const program = new Command();
64
+ registerCheckCommand(program);
65
+ await program.parseAsync(['node', 'test', 'check']);
66
+
67
+ expect(logs.join('\n')).toContain('Node.js version');
68
+ expect(logs.join('\n')).toContain('Hexabot project');
69
+ expect(logs.join('\n')).toContain('Env file .env');
70
+ expect(logs.join('\n')).toContain('Docker');
71
+ expect(process.exitCode).toBeUndefined();
72
+ });
73
+
74
+ it('supports docker-only and no-docker modes', async () => {
75
+ checkDocker.mockReturnValue({ ok: true, message: 'Docker OK' });
76
+ checkNodeVersion.mockReturnValue({ ok: true, message: 'Node OK' });
77
+
78
+ const dockerOnlyProgram = new Command();
79
+ registerCheckCommand(dockerOnlyProgram);
80
+ await dockerOnlyProgram.parseAsync([
81
+ 'node',
82
+ 'test',
83
+ 'check',
84
+ '--docker-only',
85
+ ]);
86
+
87
+ expect(checkNodeVersion).not.toHaveBeenCalled();
88
+ expect(isHexabotProject).not.toHaveBeenCalled();
89
+ expect(checkDocker).toHaveBeenCalled();
90
+
91
+ jest.clearAllMocks();
92
+ checkDocker.mockReturnValue({ ok: true, message: 'Docker OK' });
93
+ checkNodeVersion.mockReturnValue({ ok: true, message: 'Node OK' });
94
+ const noDockerProgram = new Command();
95
+ registerCheckCommand(noDockerProgram);
96
+ await noDockerProgram.parseAsync(['node', 'test', 'check', '--no-docker']);
97
+ expect(checkDocker).not.toHaveBeenCalled();
98
+ });
99
+
100
+ it('sets process exit code when checks fail', async () => {
101
+ checkNodeVersion.mockReturnValue({ ok: true, message: 'Node OK' });
102
+ isHexabotProject.mockReturnValue(false);
103
+ checkDocker.mockReturnValue({ ok: false, message: 'Docker missing' });
104
+ const logs: string[] = [];
105
+ jest.spyOn(console, 'log').mockImplementation((message?: string) => {
106
+ logs.push(String(message));
107
+ });
108
+
109
+ const program = new Command();
110
+ registerCheckCommand(program);
111
+ await program.parseAsync(['node', 'test', 'check', '--no-docker']);
112
+
113
+ expect(logs.join('\n')).toContain('Hexabot project');
114
+ expect(process.exitCode).toBe(1);
115
+ });
116
+ });
@@ -0,0 +1,97 @@
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 updateProjectConfig = jest.fn();
12
+ const normalizePackageManager = jest.fn();
13
+ const assertHexabotProject = jest.fn();
14
+
15
+ jest.unstable_mockModule('../../core/config.js', () => ({
16
+ loadProjectConfig,
17
+ updateProjectConfig,
18
+ }));
19
+
20
+ jest.unstable_mockModule('../../core/package-manager.js', () => ({
21
+ normalizePackageManager,
22
+ }));
23
+
24
+ jest.unstable_mockModule('../../core/project.js', () => ({
25
+ assertHexabotProject,
26
+ }));
27
+
28
+ let registerConfigCommand: (program: Command) => void;
29
+
30
+ beforeAll(async () => {
31
+ ({ registerConfigCommand } = await import('../config.js'));
32
+ });
33
+
34
+ beforeEach(() => {
35
+ jest.resetAllMocks();
36
+ loadProjectConfig.mockReturnValue({ packageManager: 'npm' });
37
+ normalizePackageManager.mockImplementation((value: unknown) =>
38
+ typeof value === 'string' ? value.toLowerCase() : undefined,
39
+ );
40
+ });
41
+
42
+ describe('registerConfigCommand', () => {
43
+ it('prints the current project config', async () => {
44
+ const logs: string[] = [];
45
+ jest.spyOn(console, 'log').mockImplementation((message?: string) => {
46
+ logs.push(String(message));
47
+ });
48
+ const program = new Command();
49
+ registerConfigCommand(program);
50
+
51
+ await program.parseAsync(['node', 'test', 'config', 'show']);
52
+
53
+ expect(logs.some((line) => line.includes('"packageManager"'))).toBe(true);
54
+ });
55
+
56
+ it('updates configuration values with automatic parsing', async () => {
57
+ const program = new Command();
58
+ registerConfigCommand(program);
59
+
60
+ await program.parseAsync([
61
+ 'node',
62
+ 'test',
63
+ 'config',
64
+ 'set',
65
+ 'packageManager',
66
+ 'PNPM',
67
+ ]);
68
+ expect(normalizePackageManager).toHaveBeenCalledWith('PNPM');
69
+ expect(updateProjectConfig).toHaveBeenCalledWith(expect.any(String), {
70
+ packageManager: 'pnpm',
71
+ });
72
+
73
+ await program.parseAsync([
74
+ 'node',
75
+ 'test',
76
+ 'config',
77
+ 'set',
78
+ 'docker.defaultServices',
79
+ 'api, postgres',
80
+ ]);
81
+ expect(updateProjectConfig).toHaveBeenCalledWith(expect.any(String), {
82
+ docker: { defaultServices: ['api', 'postgres'] },
83
+ });
84
+
85
+ await program.parseAsync([
86
+ 'node',
87
+ 'test',
88
+ 'config',
89
+ 'set',
90
+ 'limits.rate',
91
+ '50',
92
+ ]);
93
+ expect(updateProjectConfig).toHaveBeenCalledWith(expect.any(String), {
94
+ limits: { rate: 50 },
95
+ });
96
+ });
97
+ });
@@ -0,0 +1,151 @@
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 runDev: (options?: any) => Promise<void>;
59
+
60
+ beforeAll(async () => {
61
+ ({ runDev } = await import('../dev.js'));
62
+ });
63
+
64
+ beforeEach(() => {
65
+ jest.resetAllMocks();
66
+ normalizePackageManager.mockImplementation((value: unknown) =>
67
+ typeof value === 'string' ? value.toLowerCase() : undefined,
68
+ );
69
+ resolveComposeFile.mockReturnValue('/tmp/docker/docker-compose.yml');
70
+ generateComposeFiles.mockReturnValue('-f docker-compose.yml');
71
+ });
72
+
73
+ const baseConfig = {
74
+ devScript: 'dev',
75
+ startScript: 'start',
76
+ packageManager: 'pnpm',
77
+ docker: {
78
+ composeFile: 'docker/docker-compose.yml',
79
+ defaultServices: ['api'],
80
+ },
81
+ env: {
82
+ local: '.env',
83
+ localExample: '.env.example',
84
+ docker: '.env.docker',
85
+ dockerExample: '.env.docker.example',
86
+ },
87
+ };
88
+
89
+ describe('runDev', () => {
90
+ it('runs the dev script locally with env bootstrapping', async () => {
91
+ loadProjectConfig.mockReturnValue(baseConfig);
92
+ resolveEnvExample.mockReturnValue('.env.custom');
93
+
94
+ await runDev({ env: '.env.local' });
95
+
96
+ expect(assertHexabotProject).toHaveBeenCalled();
97
+ expect(resolveEnvExample).toHaveBeenCalledWith(
98
+ expect.any(String),
99
+ '.env.local',
100
+ baseConfig.env.localExample,
101
+ );
102
+ expect(bootstrapEnvFile).toHaveBeenCalledWith(
103
+ expect.any(String),
104
+ '.env.custom',
105
+ '.env.local',
106
+ );
107
+ expect(runPackageScript).toHaveBeenCalledWith(
108
+ 'pnpm',
109
+ 'dev',
110
+ expect.any(String),
111
+ );
112
+ expect(dockerCompose).not.toHaveBeenCalled();
113
+ });
114
+
115
+ it('updates the stored package manager when overridden', async () => {
116
+ loadProjectConfig.mockReturnValue({
117
+ ...baseConfig,
118
+ packageManager: 'npm',
119
+ });
120
+
121
+ await runDev({ pm: 'yarn', envBootstrap: false });
122
+
123
+ expect(updateProjectConfig).toHaveBeenCalledWith(expect.any(String), {
124
+ packageManager: 'yarn',
125
+ });
126
+ expect(bootstrapEnvFile).not.toHaveBeenCalled();
127
+ });
128
+
129
+ it('runs docker compose when the docker flag is provided', async () => {
130
+ loadProjectConfig.mockReturnValue(baseConfig);
131
+
132
+ await runDev({ docker: true, services: 'api,postgres', detach: true });
133
+
134
+ expect(bootstrapEnvFile).toHaveBeenCalledWith(
135
+ expect.any(String),
136
+ baseConfig.env.dockerExample,
137
+ baseConfig.env.docker,
138
+ );
139
+ expect(checkDocker).toHaveBeenCalled();
140
+ expect(ensureDockerFolder).toHaveBeenCalled();
141
+ expect(generateComposeFiles).toHaveBeenCalledWith(
142
+ '/tmp/docker/docker-compose.yml',
143
+ ['api', 'postgres'],
144
+ 'dev',
145
+ );
146
+ expect(dockerCompose).toHaveBeenCalledWith(
147
+ expect.stringContaining('up --build -d'),
148
+ );
149
+ expect(runPackageScript).not.toHaveBeenCalled();
150
+ });
151
+ });