@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,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 * as path from 'path';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import { loadProjectConfig, updateProjectConfig } from '../core/config.js';
|
|
9
|
+
import { dockerCompose, generateComposeFiles, resolveComposeFile, } from '../core/docker.js';
|
|
10
|
+
import { bootstrapEnvFile, resolveEnvExample } from '../core/env.js';
|
|
11
|
+
import { detectPackageManager, normalizePackageManager, runPackageScript, } from '../core/package-manager.js';
|
|
12
|
+
import { checkDocker } from '../core/prerequisites.js';
|
|
13
|
+
import { assertHexabotProject, ensureDockerFolder } from '../core/project.js';
|
|
14
|
+
import { parseServices } from '../utils/services.js';
|
|
15
|
+
export const registerDevCommand = (program) => {
|
|
16
|
+
program
|
|
17
|
+
.command('dev')
|
|
18
|
+
.description('Run the current Hexabot project in development mode')
|
|
19
|
+
.option('--docker', 'Run using Docker')
|
|
20
|
+
.option('--services <list>', 'Comma-separated services or profiles to enable')
|
|
21
|
+
.option('-d, --detach', 'Detach Docker containers (Docker mode only)')
|
|
22
|
+
.option('--env <file>', 'Env file to use for local dev (default: .env)')
|
|
23
|
+
.option('--no-env-bootstrap', 'Skip env bootstrapping')
|
|
24
|
+
.option('--pm <npm|pnpm|yarn|bun>', 'Override package manager')
|
|
25
|
+
.action(async (options) => {
|
|
26
|
+
await runDev(options);
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
export const runDev = async (options = {}) => {
|
|
30
|
+
const projectRoot = path.resolve(options.cwd || process.cwd());
|
|
31
|
+
assertHexabotProject(projectRoot);
|
|
32
|
+
const config = loadProjectConfig(projectRoot);
|
|
33
|
+
const normalizedPm = normalizePackageManager(options.pm);
|
|
34
|
+
const pm = normalizedPm || config.packageManager || detectPackageManager(projectRoot);
|
|
35
|
+
if (config.packageManager !== pm) {
|
|
36
|
+
updateProjectConfig(projectRoot, { packageManager: pm });
|
|
37
|
+
}
|
|
38
|
+
const shouldBootstrap = options.envBootstrap !== false;
|
|
39
|
+
if (shouldBootstrap) {
|
|
40
|
+
if (options.docker) {
|
|
41
|
+
bootstrapEnvFile(projectRoot, config.env.dockerExample, config.env.docker);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
const envFile = options.env || config.env.local;
|
|
45
|
+
const envExample = resolveEnvExample(projectRoot, envFile, config.env.localExample);
|
|
46
|
+
bootstrapEnvFile(projectRoot, envExample, envFile);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (options.docker) {
|
|
50
|
+
await runDockerDev(projectRoot, options, config);
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
runPackageScript(pm, config.devScript, projectRoot);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const runDockerDev = async (projectRoot, options, config) => {
|
|
57
|
+
checkDocker({ silent: true });
|
|
58
|
+
ensureDockerFolder(projectRoot);
|
|
59
|
+
const servicesInput = parseServices(options.services || '');
|
|
60
|
+
const services = servicesInput.length
|
|
61
|
+
? servicesInput
|
|
62
|
+
: config.docker.defaultServices;
|
|
63
|
+
const composeFile = resolveComposeFile(projectRoot, config.docker.composeFile);
|
|
64
|
+
const composeArgs = generateComposeFiles(composeFile, services, 'dev');
|
|
65
|
+
const upArgs = ['up', '--build'];
|
|
66
|
+
if (options.detach) {
|
|
67
|
+
upArgs.push('-d');
|
|
68
|
+
}
|
|
69
|
+
const composeCommand = `${composeArgs} ${upArgs.join(' ')}`.trim();
|
|
70
|
+
console.log(chalk.blue(`Starting Docker services${services.length ? ` (${services.join(', ')})` : ''}`));
|
|
71
|
+
dockerCompose(composeCommand);
|
|
72
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
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 { loadProjectConfig } from '../core/config.js';
|
|
8
|
+
import { dockerCompose, generateComposeFiles, resolveComposeFile, } from '../core/docker.js';
|
|
9
|
+
import { bootstrapEnvFile } from '../core/env.js';
|
|
10
|
+
import { checkDocker } from '../core/prerequisites.js';
|
|
11
|
+
import { assertHexabotProject, ensureDockerFolder } from '../core/project.js';
|
|
12
|
+
import { parseServices } from '../utils/services.js';
|
|
13
|
+
import { runStart } from './start.js';
|
|
14
|
+
export const registerDockerCommand = (program) => {
|
|
15
|
+
const dockerCommand = program.command('docker').description('Docker helpers');
|
|
16
|
+
dockerCommand
|
|
17
|
+
.command('up')
|
|
18
|
+
.description('Start Docker services')
|
|
19
|
+
.option('--services <list>', 'Comma-separated services/profiles')
|
|
20
|
+
.option('-d, --detach', 'Run containers in the background')
|
|
21
|
+
.option('--build', 'Build images before starting')
|
|
22
|
+
.action((options) => runDockerCommand('up', options));
|
|
23
|
+
dockerCommand
|
|
24
|
+
.command('down')
|
|
25
|
+
.description('Stop Docker services')
|
|
26
|
+
.option('--services <list>', 'Comma-separated services/profiles')
|
|
27
|
+
.option('--volumes', 'Remove volumes')
|
|
28
|
+
.action((options) => runDockerCommand('down', options));
|
|
29
|
+
dockerCommand
|
|
30
|
+
.command('logs [service]')
|
|
31
|
+
.description('View Docker logs')
|
|
32
|
+
.option('-f, --follow', 'Follow logs')
|
|
33
|
+
.option('--since <time>', 'Only show logs since time (e.g. 1h)')
|
|
34
|
+
.action((service, options) => runDockerLogs(service, options));
|
|
35
|
+
dockerCommand
|
|
36
|
+
.command('ps')
|
|
37
|
+
.description('List running services')
|
|
38
|
+
.action(() => runDockerPs());
|
|
39
|
+
dockerCommand
|
|
40
|
+
.command('start')
|
|
41
|
+
.description('Start Docker services in production mode')
|
|
42
|
+
.option('--services <list>', 'Comma-separated services/profiles')
|
|
43
|
+
.option('-d, --detach', 'Run containers in the background')
|
|
44
|
+
.option('--build', 'Build images before starting')
|
|
45
|
+
.option('--env-bootstrap', 'Generate env files from *.example if missing')
|
|
46
|
+
.action(async (options) => {
|
|
47
|
+
await runStart({
|
|
48
|
+
docker: true,
|
|
49
|
+
services: options.services,
|
|
50
|
+
detach: options.detach,
|
|
51
|
+
build: options.build,
|
|
52
|
+
envBootstrap: options.envBootstrap,
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
};
|
|
56
|
+
const runDockerCommand = (lifecycle, options) => {
|
|
57
|
+
const projectRoot = path.resolve(process.cwd());
|
|
58
|
+
assertHexabotProject(projectRoot);
|
|
59
|
+
checkDocker({ silent: true });
|
|
60
|
+
const config = loadProjectConfig(projectRoot);
|
|
61
|
+
ensureDockerFolder(projectRoot);
|
|
62
|
+
const services = resolveServices(options.services, config);
|
|
63
|
+
const composeFile = resolveComposeFile(projectRoot, config.docker.composeFile);
|
|
64
|
+
const composeArgs = generateComposeFiles(composeFile, services, lifecycle === 'up' ? 'dev' : undefined);
|
|
65
|
+
if (lifecycle === 'up') {
|
|
66
|
+
bootstrapEnvFile(projectRoot, config.env.dockerExample, config.env.docker, {
|
|
67
|
+
quiet: true,
|
|
68
|
+
});
|
|
69
|
+
const commandArgs = ['up'];
|
|
70
|
+
if (options.build) {
|
|
71
|
+
commandArgs.push('--build');
|
|
72
|
+
}
|
|
73
|
+
if (options.detach) {
|
|
74
|
+
commandArgs.push('-d');
|
|
75
|
+
}
|
|
76
|
+
dockerCompose(`${composeArgs} ${commandArgs.join(' ')}`.trim());
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const commandArgs = ['down'];
|
|
80
|
+
if (options.volumes) {
|
|
81
|
+
commandArgs.push('-v');
|
|
82
|
+
}
|
|
83
|
+
dockerCompose(`${composeArgs} ${commandArgs.join(' ')}`.trim());
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
const runDockerLogs = (service, options) => {
|
|
87
|
+
const projectRoot = path.resolve(process.cwd());
|
|
88
|
+
assertHexabotProject(projectRoot);
|
|
89
|
+
checkDocker({ silent: true });
|
|
90
|
+
const config = loadProjectConfig(projectRoot);
|
|
91
|
+
ensureDockerFolder(projectRoot);
|
|
92
|
+
const composeFile = resolveComposeFile(projectRoot, config.docker.composeFile);
|
|
93
|
+
const composeArgs = generateComposeFiles(composeFile, config.docker.defaultServices, 'dev');
|
|
94
|
+
const args = ['logs'];
|
|
95
|
+
if (options.follow) {
|
|
96
|
+
args.push('-f');
|
|
97
|
+
}
|
|
98
|
+
if (options.since) {
|
|
99
|
+
args.push(`--since ${options.since}`);
|
|
100
|
+
}
|
|
101
|
+
if (service) {
|
|
102
|
+
args.push(service);
|
|
103
|
+
}
|
|
104
|
+
dockerCompose(`${composeArgs} ${args.join(' ')}`.trim());
|
|
105
|
+
};
|
|
106
|
+
const runDockerPs = () => {
|
|
107
|
+
const projectRoot = path.resolve(process.cwd());
|
|
108
|
+
assertHexabotProject(projectRoot);
|
|
109
|
+
checkDocker({ silent: true });
|
|
110
|
+
const config = loadProjectConfig(projectRoot);
|
|
111
|
+
ensureDockerFolder(projectRoot);
|
|
112
|
+
const composeFile = resolveComposeFile(projectRoot, config.docker.composeFile);
|
|
113
|
+
const composeArgs = generateComposeFiles(composeFile, config.docker.defaultServices, 'dev');
|
|
114
|
+
dockerCompose(`${composeArgs} ps`);
|
|
115
|
+
};
|
|
116
|
+
const resolveServices = (servicesInput, config) => {
|
|
117
|
+
const provided = parseServices(servicesInput || '');
|
|
118
|
+
return provided.length ? provided : config.docker.defaultServices;
|
|
119
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
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 } from '../core/config.js';
|
|
9
|
+
import { bootstrapEnvFile, listEnvStatus } from '../core/env.js';
|
|
10
|
+
import { assertHexabotProject } from '../core/project.js';
|
|
11
|
+
export const registerEnvCommand = (program) => {
|
|
12
|
+
const envCommand = program.command('env').description('Manage env files');
|
|
13
|
+
envCommand
|
|
14
|
+
.command('init')
|
|
15
|
+
.description('Copy *.example env files')
|
|
16
|
+
.option('--docker', 'Initialize Docker env file')
|
|
17
|
+
.option('--force', 'Overwrite existing env files')
|
|
18
|
+
.action((options) => {
|
|
19
|
+
const projectRoot = path.resolve(process.cwd());
|
|
20
|
+
assertHexabotProject(projectRoot);
|
|
21
|
+
const config = loadProjectConfig(projectRoot);
|
|
22
|
+
if (options.docker) {
|
|
23
|
+
bootstrapEnvFile(projectRoot, config.env.dockerExample, config.env.docker, { force: options.force });
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
bootstrapEnvFile(projectRoot, config.env.localExample, config.env.local, {
|
|
27
|
+
force: options.force,
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
envCommand
|
|
32
|
+
.command('list')
|
|
33
|
+
.description('Show env file status')
|
|
34
|
+
.action(() => {
|
|
35
|
+
const projectRoot = path.resolve(process.cwd());
|
|
36
|
+
assertHexabotProject(projectRoot);
|
|
37
|
+
const config = loadProjectConfig(projectRoot);
|
|
38
|
+
const statuses = listEnvStatus(projectRoot, config);
|
|
39
|
+
statuses.forEach((status) => {
|
|
40
|
+
const symbol = status.exists ? chalk.green('✓') : chalk.red('✗');
|
|
41
|
+
console.log(`${symbol} ${status.file}`);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import { dockerExec } from '../core/docker.js';
|
|
7
|
+
import { checkDocker } from '../core/prerequisites.js';
|
|
8
|
+
import { ensureDockerFolder } from '../core/project.js';
|
|
9
|
+
export const registerMigrateCommand = (program) => {
|
|
10
|
+
program
|
|
11
|
+
.command('migrate [args...]')
|
|
12
|
+
.description('Run database migrations')
|
|
13
|
+
.action((args = []) => {
|
|
14
|
+
checkDocker({ silent: true });
|
|
15
|
+
ensureDockerFolder();
|
|
16
|
+
const migrateArgs = args.join(' ').trim();
|
|
17
|
+
const command = migrateArgs
|
|
18
|
+
? `npm run migrate ${migrateArgs}`
|
|
19
|
+
: 'npm run migrate';
|
|
20
|
+
dockerExec('api', command, '--user $(id -u):$(id -g)');
|
|
21
|
+
});
|
|
22
|
+
};
|
|
@@ -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 { dockerCompose, generateComposeFiles, resolveComposeFile, } from '../core/docker.js';
|
|
10
|
+
import { bootstrapEnvFile, resolveEnvExample } from '../core/env.js';
|
|
11
|
+
import { detectPackageManager, normalizePackageManager, runPackageScript, } from '../core/package-manager.js';
|
|
12
|
+
import { checkDocker } from '../core/prerequisites.js';
|
|
13
|
+
import { assertHexabotProject, ensureDockerFolder } from '../core/project.js';
|
|
14
|
+
import { parseServices } from '../utils/services.js';
|
|
15
|
+
export const registerStartCommand = (program) => {
|
|
16
|
+
program
|
|
17
|
+
.command('start')
|
|
18
|
+
.description('Run the project in production mode')
|
|
19
|
+
.option('--docker', 'Run using Docker (production stack)')
|
|
20
|
+
.option('--services <list>', 'Comma-separated services/profiles to enable')
|
|
21
|
+
.option('-d, --detach', 'Detach Docker containers')
|
|
22
|
+
.option('--build', 'Build Docker images before starting')
|
|
23
|
+
.option('--env <file>', 'Env file to use (default: .env)')
|
|
24
|
+
.option('--env-bootstrap', 'Generate env files from *.example if missing')
|
|
25
|
+
.option('--pm <npm|pnpm|yarn|bun>', 'Override package manager')
|
|
26
|
+
.action(async (options) => {
|
|
27
|
+
await runStart(options);
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
export const runStart = async (options = {}) => {
|
|
31
|
+
const projectRoot = path.resolve(options.cwd || process.cwd());
|
|
32
|
+
assertHexabotProject(projectRoot);
|
|
33
|
+
const config = loadProjectConfig(projectRoot);
|
|
34
|
+
const normalizedPm = normalizePackageManager(options.pm);
|
|
35
|
+
const pm = normalizedPm || config.packageManager || detectPackageManager(projectRoot);
|
|
36
|
+
if (config.packageManager !== pm) {
|
|
37
|
+
updateProjectConfig(projectRoot, { packageManager: pm });
|
|
38
|
+
}
|
|
39
|
+
const shouldBootstrap = Boolean(options.envBootstrap);
|
|
40
|
+
if (shouldBootstrap) {
|
|
41
|
+
if (options.docker) {
|
|
42
|
+
bootstrapEnvFile(projectRoot, config.env.dockerExample, config.env.docker);
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
const envFile = options.env || config.env.local;
|
|
46
|
+
const envExample = resolveEnvExample(projectRoot, envFile, config.env.localExample);
|
|
47
|
+
bootstrapEnvFile(projectRoot, envExample, envFile);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (options.docker) {
|
|
51
|
+
await runDockerStart(projectRoot, options, config);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
runPackageScript(pm, config.startScript, projectRoot);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const runDockerStart = async (projectRoot, options, config) => {
|
|
58
|
+
checkDocker({ silent: true });
|
|
59
|
+
ensureDockerFolder(projectRoot);
|
|
60
|
+
const servicesInput = parseServices(options.services || '');
|
|
61
|
+
const services = servicesInput.length
|
|
62
|
+
? servicesInput
|
|
63
|
+
: config.docker.defaultServices;
|
|
64
|
+
const composeFile = resolveComposeFile(projectRoot, config.docker.composeFile);
|
|
65
|
+
const composeArgs = generateComposeFiles(composeFile, services, 'prod');
|
|
66
|
+
const upArgs = ['up'];
|
|
67
|
+
if (options.build) {
|
|
68
|
+
upArgs.push('--build');
|
|
69
|
+
}
|
|
70
|
+
if (options.detach) {
|
|
71
|
+
upArgs.push('-d');
|
|
72
|
+
}
|
|
73
|
+
const composeCommand = `${composeArgs} ${upArgs.join(' ')}`.trim();
|
|
74
|
+
console.log(chalk.blue(`Starting Docker services in production mode${services.length ? ` (${services.join(', ')})` : ''}`));
|
|
75
|
+
dockerCompose(composeCommand);
|
|
76
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { ensureProjectConfig, loadProjectConfig, resolveConfigPath, updateProjectConfig, } from '../config.js';
|
|
10
|
+
describe('project config helpers', () => {
|
|
11
|
+
let tempDir;
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hexabot-config-'));
|
|
14
|
+
});
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
it('loads default values when the config file is missing', () => {
|
|
19
|
+
const config = loadProjectConfig(tempDir);
|
|
20
|
+
expect(config).toMatchObject({
|
|
21
|
+
devScript: 'dev',
|
|
22
|
+
startScript: 'start',
|
|
23
|
+
docker: {
|
|
24
|
+
composeFile: 'docker/docker-compose.yml',
|
|
25
|
+
defaultServices: [],
|
|
26
|
+
},
|
|
27
|
+
env: {
|
|
28
|
+
local: '.env',
|
|
29
|
+
localExample: '.env.example',
|
|
30
|
+
docker: '.env.docker',
|
|
31
|
+
dockerExample: '.env.docker.example',
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
it('merges overrides from the config file', () => {
|
|
36
|
+
const override = {
|
|
37
|
+
devScript: 'custom-dev',
|
|
38
|
+
docker: {
|
|
39
|
+
composeFile: 'docker/compose.override.yml',
|
|
40
|
+
defaultServices: ['api', 'postgres'],
|
|
41
|
+
},
|
|
42
|
+
env: {
|
|
43
|
+
local: '.env.dev',
|
|
44
|
+
localExample: '.env.example',
|
|
45
|
+
docker: '.env.docker',
|
|
46
|
+
dockerExample: '.env.docker.example',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
fs.writeFileSync(resolveConfigPath(tempDir), JSON.stringify(override));
|
|
50
|
+
const config = loadProjectConfig(tempDir);
|
|
51
|
+
expect(config.devScript).toBe('custom-dev');
|
|
52
|
+
expect(config.docker).toMatchObject({
|
|
53
|
+
composeFile: 'docker/compose.override.yml',
|
|
54
|
+
defaultServices: ['api', 'postgres'],
|
|
55
|
+
});
|
|
56
|
+
expect(config.env.local).toBe('.env.dev');
|
|
57
|
+
expect(config.env.localExample).toBe('.env.example');
|
|
58
|
+
});
|
|
59
|
+
it('creates a config file when ensuring one with overrides', () => {
|
|
60
|
+
const overrides = {
|
|
61
|
+
packageManager: 'pnpm',
|
|
62
|
+
env: {
|
|
63
|
+
local: '.env.dev',
|
|
64
|
+
localExample: '.env.example',
|
|
65
|
+
docker: '.env.docker',
|
|
66
|
+
dockerExample: '.env.docker.example',
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const config = ensureProjectConfig(tempDir, overrides);
|
|
70
|
+
expect(config.packageManager).toBe('pnpm');
|
|
71
|
+
expect(JSON.parse(fs.readFileSync(resolveConfigPath(tempDir), 'utf-8'))).toMatchObject({
|
|
72
|
+
packageManager: 'pnpm',
|
|
73
|
+
env: expect.objectContaining({ local: '.env.dev' }),
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
it('updates existing config files by merging overrides', () => {
|
|
77
|
+
fs.writeFileSync(resolveConfigPath(tempDir), JSON.stringify({ startScript: 'serve' }));
|
|
78
|
+
const updated = updateProjectConfig(tempDir, {
|
|
79
|
+
docker: {
|
|
80
|
+
composeFile: 'docker/docker-compose.yml',
|
|
81
|
+
defaultServices: ['api'],
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
expect(updated.startScript).toBe('serve');
|
|
85
|
+
expect(updated.docker.defaultServices).toEqual(['api']);
|
|
86
|
+
expect(updated.env.local).toBe('.env');
|
|
87
|
+
});
|
|
88
|
+
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { jest } from '@jest/globals';
|
|
10
|
+
import { generateComposeFiles } from '../docker.js';
|
|
11
|
+
describe('generateComposeFiles', () => {
|
|
12
|
+
let tempDir;
|
|
13
|
+
let composeFile;
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hexabot-docker-'));
|
|
16
|
+
composeFile = path.join(tempDir, 'docker-compose.yml');
|
|
17
|
+
fs.writeFileSync(composeFile, '');
|
|
18
|
+
});
|
|
19
|
+
afterEach(() => {
|
|
20
|
+
jest.restoreAllMocks();
|
|
21
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
22
|
+
});
|
|
23
|
+
it('builds the compose file list for provided services', () => {
|
|
24
|
+
const apiFile = path.join(tempDir, 'docker-compose.api.yml');
|
|
25
|
+
const dbFile = path.join(tempDir, 'docker-compose.db.yml');
|
|
26
|
+
fs.writeFileSync(apiFile, '');
|
|
27
|
+
fs.writeFileSync(dbFile, '');
|
|
28
|
+
const result = generateComposeFiles(composeFile, ['api', 'db']);
|
|
29
|
+
expect(result).toBe(`-f ${composeFile} ` + `-f ${apiFile} ` + `-f ${dbFile}`);
|
|
30
|
+
});
|
|
31
|
+
it('includes mode specific service and main files when they exist', () => {
|
|
32
|
+
const apiFile = path.join(tempDir, 'docker-compose.api.yml');
|
|
33
|
+
const serviceModeFile = path.join(tempDir, 'docker-compose.api.dev.yml');
|
|
34
|
+
const mainModeFile = path.join(tempDir, 'docker-compose.dev.yml');
|
|
35
|
+
fs.writeFileSync(apiFile, '');
|
|
36
|
+
fs.writeFileSync(serviceModeFile, '');
|
|
37
|
+
fs.writeFileSync(mainModeFile, '');
|
|
38
|
+
const result = generateComposeFiles(composeFile, ['api'], 'dev');
|
|
39
|
+
expect(result).toBe(`-f ${composeFile} ` +
|
|
40
|
+
`-f ${apiFile} ` +
|
|
41
|
+
`-f ${serviceModeFile} -f ${mainModeFile}`);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import fs from 'fs';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { jest } from '@jest/globals';
|
|
10
|
+
import { bootstrapEnvFile, listEnvStatus, resolveEnvExample } from '../env.js';
|
|
11
|
+
describe('env helpers', () => {
|
|
12
|
+
let tempDir;
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hexabot-env-'));
|
|
15
|
+
});
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
18
|
+
jest.restoreAllMocks();
|
|
19
|
+
});
|
|
20
|
+
it('copies the example env file when the target is missing', () => {
|
|
21
|
+
const example = '.env.example';
|
|
22
|
+
const target = '.env';
|
|
23
|
+
fs.writeFileSync(path.join(tempDir, example), 'KEY=value');
|
|
24
|
+
const result = bootstrapEnvFile(tempDir, example, target);
|
|
25
|
+
expect(result).toBe(true);
|
|
26
|
+
expect(fs.readFileSync(path.join(tempDir, target), 'utf-8')).toBe('KEY=value');
|
|
27
|
+
});
|
|
28
|
+
it('skips copying when the target exists unless forced', () => {
|
|
29
|
+
const example = '.env.example';
|
|
30
|
+
const target = '.env';
|
|
31
|
+
fs.writeFileSync(path.join(tempDir, example), 'NEW=value');
|
|
32
|
+
fs.writeFileSync(path.join(tempDir, target), 'OLD=value');
|
|
33
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
34
|
+
const skipped = bootstrapEnvFile(tempDir, example, target, {
|
|
35
|
+
quiet: false,
|
|
36
|
+
});
|
|
37
|
+
expect(skipped).toBe(false);
|
|
38
|
+
expect(fs.readFileSync(path.join(tempDir, target), 'utf-8')).toBe('OLD=value');
|
|
39
|
+
expect(logSpy).toHaveBeenCalled();
|
|
40
|
+
const forced = bootstrapEnvFile(tempDir, example, target, { force: true });
|
|
41
|
+
expect(forced).toBe(true);
|
|
42
|
+
expect(fs.readFileSync(path.join(tempDir, target), 'utf-8')).toBe('NEW=value');
|
|
43
|
+
});
|
|
44
|
+
it('lists env file statuses using the provided config', () => {
|
|
45
|
+
const config = {
|
|
46
|
+
env: {
|
|
47
|
+
local: '.env',
|
|
48
|
+
localExample: '.env.example',
|
|
49
|
+
docker: '.env.docker',
|
|
50
|
+
dockerExample: '.env.docker.example',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
fs.writeFileSync(path.join(tempDir, '.env.example'), '');
|
|
54
|
+
fs.writeFileSync(path.join(tempDir, '.env'), '');
|
|
55
|
+
const statuses = listEnvStatus(tempDir, config);
|
|
56
|
+
expect(statuses).toEqual([
|
|
57
|
+
expect.objectContaining({ file: '.env.example', exists: true }),
|
|
58
|
+
expect.objectContaining({ file: '.env', exists: true }),
|
|
59
|
+
expect.objectContaining({ file: '.env.docker.example', exists: false }),
|
|
60
|
+
expect.objectContaining({ file: '.env.docker', exists: false }),
|
|
61
|
+
]);
|
|
62
|
+
});
|
|
63
|
+
it('resolves env example files relative to the project root', () => {
|
|
64
|
+
const envFile = '.env.local';
|
|
65
|
+
const defaultExample = '.env.example';
|
|
66
|
+
const examplePath = path.join(tempDir, `${envFile}.example`);
|
|
67
|
+
fs.writeFileSync(examplePath, '');
|
|
68
|
+
expect(resolveEnvExample(tempDir, envFile, defaultExample)).toBe(`${envFile}.example`);
|
|
69
|
+
expect(resolveEnvExample(tempDir, '.missing', defaultExample)).toBe(defaultExample);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -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
|
+
import fs from 'fs';
|
|
7
|
+
import * as os from 'os';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { jest } from '@jest/globals';
|
|
10
|
+
const readPackageJson = jest.fn();
|
|
11
|
+
const execSync = jest.fn();
|
|
12
|
+
jest.unstable_mockModule('../project.js', () => ({
|
|
13
|
+
readPackageJson,
|
|
14
|
+
}));
|
|
15
|
+
jest.unstable_mockModule('child_process', () => ({
|
|
16
|
+
execSync,
|
|
17
|
+
}));
|
|
18
|
+
let normalizePackageManager;
|
|
19
|
+
let detectPackageManager;
|
|
20
|
+
let runPackageScript;
|
|
21
|
+
let installDependencies;
|
|
22
|
+
beforeAll(async () => {
|
|
23
|
+
({
|
|
24
|
+
normalizePackageManager,
|
|
25
|
+
detectPackageManager,
|
|
26
|
+
runPackageScript,
|
|
27
|
+
installDependencies,
|
|
28
|
+
} = await import('../package-manager.js'));
|
|
29
|
+
});
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
jest.resetAllMocks();
|
|
32
|
+
});
|
|
33
|
+
describe('normalizePackageManager', () => {
|
|
34
|
+
it('returns normalized package manager names', () => {
|
|
35
|
+
expect(normalizePackageManager('PNPM')).toBe('pnpm');
|
|
36
|
+
expect(normalizePackageManager(undefined)).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
it('exits when the package manager is unsupported', () => {
|
|
39
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => {
|
|
40
|
+
throw new Error('exit:1');
|
|
41
|
+
}));
|
|
42
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
43
|
+
expect(() => normalizePackageManager('foo')).toThrow('exit:1');
|
|
44
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
describe('detectPackageManager', () => {
|
|
48
|
+
let tempDir;
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hexabot-pm-'));
|
|
51
|
+
});
|
|
52
|
+
afterEach(() => {
|
|
53
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
54
|
+
});
|
|
55
|
+
it('detects the package manager based on lockfiles', () => {
|
|
56
|
+
fs.writeFileSync(path.join(tempDir, 'pnpm-lock.yaml'), '');
|
|
57
|
+
expect(detectPackageManager(tempDir)).toBe('pnpm');
|
|
58
|
+
});
|
|
59
|
+
it('falls back to npm when no lockfiles are found', () => {
|
|
60
|
+
expect(detectPackageManager(tempDir)).toBe('npm');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
describe('installDependencies & runPackageScript', () => {
|
|
64
|
+
const projectRoot = '/tmp/project';
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
readPackageJson.mockReturnValue({
|
|
67
|
+
scripts: {
|
|
68
|
+
build: 'tsc',
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
it('runs installation commands for the chosen package manager', () => {
|
|
73
|
+
installDependencies('yarn', projectRoot);
|
|
74
|
+
expect(execSync).toHaveBeenCalledWith('yarn install', {
|
|
75
|
+
cwd: projectRoot,
|
|
76
|
+
stdio: 'inherit',
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
it('runs package scripts after ensuring the script exists', () => {
|
|
80
|
+
runPackageScript('pnpm', 'build', projectRoot, ['--watch']);
|
|
81
|
+
expect(execSync).toHaveBeenCalledWith('pnpm run build -- --watch', {
|
|
82
|
+
cwd: projectRoot,
|
|
83
|
+
stdio: 'inherit',
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
it('exits when the script is missing from package.json', () => {
|
|
87
|
+
readPackageJson.mockReturnValue({ scripts: {} });
|
|
88
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => {
|
|
89
|
+
throw new Error('exit:1');
|
|
90
|
+
}));
|
|
91
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
92
|
+
expect(() => runPackageScript('npm', 'dev', projectRoot)).toThrow('exit:1');
|
|
93
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
94
|
+
});
|
|
95
|
+
});
|