@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,126 @@
|
|
|
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 {
|
|
14
|
+
dockerCompose,
|
|
15
|
+
generateComposeFiles,
|
|
16
|
+
resolveComposeFile,
|
|
17
|
+
} from '../core/docker.js';
|
|
18
|
+
import { bootstrapEnvFile, resolveEnvExample } from '../core/env.js';
|
|
19
|
+
import {
|
|
20
|
+
detectPackageManager,
|
|
21
|
+
normalizePackageManager,
|
|
22
|
+
runPackageScript,
|
|
23
|
+
} from '../core/package-manager.js';
|
|
24
|
+
import { checkDocker } from '../core/prerequisites.js';
|
|
25
|
+
import { assertHexabotProject, ensureDockerFolder } from '../core/project.js';
|
|
26
|
+
import { parseServices } from '../utils/services.js';
|
|
27
|
+
|
|
28
|
+
export interface StartOptions {
|
|
29
|
+
docker?: boolean;
|
|
30
|
+
services?: string;
|
|
31
|
+
detach?: boolean;
|
|
32
|
+
build?: boolean;
|
|
33
|
+
env?: string;
|
|
34
|
+
envBootstrap?: boolean;
|
|
35
|
+
pm?: string;
|
|
36
|
+
cwd?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const registerStartCommand = (program: Command) => {
|
|
40
|
+
program
|
|
41
|
+
.command('start')
|
|
42
|
+
.description('Run the project in production mode')
|
|
43
|
+
.option('--docker', 'Run using Docker (production stack)')
|
|
44
|
+
.option('--services <list>', 'Comma-separated services/profiles to enable')
|
|
45
|
+
.option('-d, --detach', 'Detach Docker containers')
|
|
46
|
+
.option('--build', 'Build Docker images before starting')
|
|
47
|
+
.option('--env <file>', 'Env file to use (default: .env)')
|
|
48
|
+
.option('--env-bootstrap', 'Generate env files from *.example if missing')
|
|
49
|
+
.option('--pm <npm|pnpm|yarn|bun>', 'Override package manager')
|
|
50
|
+
.action(async (options: StartOptions) => {
|
|
51
|
+
await runStart(options);
|
|
52
|
+
});
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export const runStart = async (options: StartOptions = {}) => {
|
|
56
|
+
const projectRoot = path.resolve(options.cwd || process.cwd());
|
|
57
|
+
assertHexabotProject(projectRoot);
|
|
58
|
+
const config = loadProjectConfig(projectRoot);
|
|
59
|
+
const normalizedPm = normalizePackageManager(options.pm);
|
|
60
|
+
const pm =
|
|
61
|
+
normalizedPm || config.packageManager || detectPackageManager(projectRoot);
|
|
62
|
+
|
|
63
|
+
if (config.packageManager !== pm) {
|
|
64
|
+
updateProjectConfig(projectRoot, { packageManager: pm });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const shouldBootstrap = Boolean(options.envBootstrap);
|
|
68
|
+
if (shouldBootstrap) {
|
|
69
|
+
if (options.docker) {
|
|
70
|
+
bootstrapEnvFile(
|
|
71
|
+
projectRoot,
|
|
72
|
+
config.env.dockerExample,
|
|
73
|
+
config.env.docker,
|
|
74
|
+
);
|
|
75
|
+
} else {
|
|
76
|
+
const envFile = options.env || config.env.local;
|
|
77
|
+
const envExample = resolveEnvExample(
|
|
78
|
+
projectRoot,
|
|
79
|
+
envFile,
|
|
80
|
+
config.env.localExample,
|
|
81
|
+
);
|
|
82
|
+
bootstrapEnvFile(projectRoot, envExample, envFile);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (options.docker) {
|
|
87
|
+
await runDockerStart(projectRoot, options, config);
|
|
88
|
+
} else {
|
|
89
|
+
runPackageScript(pm, config.startScript, projectRoot);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const runDockerStart = async (
|
|
94
|
+
projectRoot: string,
|
|
95
|
+
options: StartOptions,
|
|
96
|
+
config: ReturnType<typeof loadProjectConfig>,
|
|
97
|
+
) => {
|
|
98
|
+
checkDocker({ silent: true });
|
|
99
|
+
ensureDockerFolder(projectRoot);
|
|
100
|
+
const servicesInput = parseServices(options.services || '');
|
|
101
|
+
const services = servicesInput.length
|
|
102
|
+
? servicesInput
|
|
103
|
+
: config.docker.defaultServices;
|
|
104
|
+
const composeFile = resolveComposeFile(
|
|
105
|
+
projectRoot,
|
|
106
|
+
config.docker.composeFile,
|
|
107
|
+
);
|
|
108
|
+
const composeArgs = generateComposeFiles(composeFile, services, 'prod');
|
|
109
|
+
const upArgs = ['up'];
|
|
110
|
+
if (options.build) {
|
|
111
|
+
upArgs.push('--build');
|
|
112
|
+
}
|
|
113
|
+
if (options.detach) {
|
|
114
|
+
upArgs.push('-d');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const composeCommand = `${composeArgs} ${upArgs.join(' ')}`.trim();
|
|
118
|
+
console.log(
|
|
119
|
+
chalk.blue(
|
|
120
|
+
`Starting Docker services in production mode${
|
|
121
|
+
services.length ? ` (${services.join(', ')})` : ''
|
|
122
|
+
}`,
|
|
123
|
+
),
|
|
124
|
+
);
|
|
125
|
+
dockerCompose(composeCommand);
|
|
126
|
+
};
|
|
@@ -0,0 +1,114 @@
|
|
|
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 fs from 'fs';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
ensureProjectConfig,
|
|
13
|
+
HexabotConfig,
|
|
14
|
+
loadProjectConfig,
|
|
15
|
+
resolveConfigPath,
|
|
16
|
+
updateProjectConfig,
|
|
17
|
+
} from '../config.js';
|
|
18
|
+
|
|
19
|
+
describe('project config helpers', () => {
|
|
20
|
+
let tempDir: string;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hexabot-config-'));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('loads default values when the config file is missing', () => {
|
|
31
|
+
const config = loadProjectConfig(tempDir);
|
|
32
|
+
|
|
33
|
+
expect(config).toMatchObject({
|
|
34
|
+
devScript: 'dev',
|
|
35
|
+
startScript: 'start',
|
|
36
|
+
docker: {
|
|
37
|
+
composeFile: 'docker/docker-compose.yml',
|
|
38
|
+
defaultServices: [],
|
|
39
|
+
},
|
|
40
|
+
env: {
|
|
41
|
+
local: '.env',
|
|
42
|
+
localExample: '.env.example',
|
|
43
|
+
docker: '.env.docker',
|
|
44
|
+
dockerExample: '.env.docker.example',
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('merges overrides from the config file', () => {
|
|
50
|
+
const override: Partial<HexabotConfig> = {
|
|
51
|
+
devScript: 'custom-dev',
|
|
52
|
+
docker: {
|
|
53
|
+
composeFile: 'docker/compose.override.yml',
|
|
54
|
+
defaultServices: ['api', 'postgres'],
|
|
55
|
+
},
|
|
56
|
+
env: {
|
|
57
|
+
local: '.env.dev',
|
|
58
|
+
localExample: '.env.example',
|
|
59
|
+
docker: '.env.docker',
|
|
60
|
+
dockerExample: '.env.docker.example',
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
fs.writeFileSync(resolveConfigPath(tempDir), JSON.stringify(override));
|
|
64
|
+
|
|
65
|
+
const config = loadProjectConfig(tempDir);
|
|
66
|
+
|
|
67
|
+
expect(config.devScript).toBe('custom-dev');
|
|
68
|
+
expect(config.docker).toMatchObject({
|
|
69
|
+
composeFile: 'docker/compose.override.yml',
|
|
70
|
+
defaultServices: ['api', 'postgres'],
|
|
71
|
+
});
|
|
72
|
+
expect(config.env.local).toBe('.env.dev');
|
|
73
|
+
expect(config.env.localExample).toBe('.env.example');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('creates a config file when ensuring one with overrides', () => {
|
|
77
|
+
const overrides: Partial<HexabotConfig> = {
|
|
78
|
+
packageManager: 'pnpm',
|
|
79
|
+
env: {
|
|
80
|
+
local: '.env.dev',
|
|
81
|
+
localExample: '.env.example',
|
|
82
|
+
docker: '.env.docker',
|
|
83
|
+
dockerExample: '.env.docker.example',
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
const config = ensureProjectConfig(tempDir, overrides);
|
|
87
|
+
|
|
88
|
+
expect(config.packageManager).toBe('pnpm');
|
|
89
|
+
expect(
|
|
90
|
+
JSON.parse(fs.readFileSync(resolveConfigPath(tempDir), 'utf-8')),
|
|
91
|
+
).toMatchObject({
|
|
92
|
+
packageManager: 'pnpm',
|
|
93
|
+
env: expect.objectContaining({ local: '.env.dev' }),
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('updates existing config files by merging overrides', () => {
|
|
98
|
+
fs.writeFileSync(
|
|
99
|
+
resolveConfigPath(tempDir),
|
|
100
|
+
JSON.stringify({ startScript: 'serve' }),
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const updated = updateProjectConfig(tempDir, {
|
|
104
|
+
docker: {
|
|
105
|
+
composeFile: 'docker/docker-compose.yml',
|
|
106
|
+
defaultServices: ['api'],
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
expect(updated.startScript).toBe('serve');
|
|
111
|
+
expect(updated.docker.defaultServices).toEqual(['api']);
|
|
112
|
+
expect(updated.env.local).toBe('.env');
|
|
113
|
+
});
|
|
114
|
+
});
|
|
@@ -0,0 +1,59 @@
|
|
|
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 fs from 'fs';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
|
|
11
|
+
import { jest } from '@jest/globals';
|
|
12
|
+
|
|
13
|
+
import { generateComposeFiles } from '../docker.js';
|
|
14
|
+
|
|
15
|
+
describe('generateComposeFiles', () => {
|
|
16
|
+
let tempDir: string;
|
|
17
|
+
let composeFile: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hexabot-docker-'));
|
|
21
|
+
composeFile = path.join(tempDir, 'docker-compose.yml');
|
|
22
|
+
fs.writeFileSync(composeFile, '');
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
jest.restoreAllMocks();
|
|
27
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('builds the compose file list for provided services', () => {
|
|
31
|
+
const apiFile = path.join(tempDir, 'docker-compose.api.yml');
|
|
32
|
+
const dbFile = path.join(tempDir, 'docker-compose.db.yml');
|
|
33
|
+
fs.writeFileSync(apiFile, '');
|
|
34
|
+
fs.writeFileSync(dbFile, '');
|
|
35
|
+
|
|
36
|
+
const result = generateComposeFiles(composeFile, ['api', 'db']);
|
|
37
|
+
|
|
38
|
+
expect(result).toBe(
|
|
39
|
+
`-f ${composeFile} ` + `-f ${apiFile} ` + `-f ${dbFile}`,
|
|
40
|
+
);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('includes mode specific service and main files when they exist', () => {
|
|
44
|
+
const apiFile = path.join(tempDir, 'docker-compose.api.yml');
|
|
45
|
+
const serviceModeFile = path.join(tempDir, 'docker-compose.api.dev.yml');
|
|
46
|
+
const mainModeFile = path.join(tempDir, 'docker-compose.dev.yml');
|
|
47
|
+
fs.writeFileSync(apiFile, '');
|
|
48
|
+
fs.writeFileSync(serviceModeFile, '');
|
|
49
|
+
fs.writeFileSync(mainModeFile, '');
|
|
50
|
+
|
|
51
|
+
const result = generateComposeFiles(composeFile, ['api'], 'dev');
|
|
52
|
+
|
|
53
|
+
expect(result).toBe(
|
|
54
|
+
`-f ${composeFile} ` +
|
|
55
|
+
`-f ${apiFile} ` +
|
|
56
|
+
`-f ${serviceModeFile} -f ${mainModeFile}`,
|
|
57
|
+
);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
@@ -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 fs from 'fs';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
|
|
11
|
+
import { jest } from '@jest/globals';
|
|
12
|
+
|
|
13
|
+
import { bootstrapEnvFile, listEnvStatus, resolveEnvExample } from '../env.js';
|
|
14
|
+
|
|
15
|
+
describe('env helpers', () => {
|
|
16
|
+
let tempDir: string;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hexabot-env-'));
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
afterEach(() => {
|
|
23
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
24
|
+
jest.restoreAllMocks();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('copies the example env file when the target is missing', () => {
|
|
28
|
+
const example = '.env.example';
|
|
29
|
+
const target = '.env';
|
|
30
|
+
fs.writeFileSync(path.join(tempDir, example), 'KEY=value');
|
|
31
|
+
|
|
32
|
+
const result = bootstrapEnvFile(tempDir, example, target);
|
|
33
|
+
|
|
34
|
+
expect(result).toBe(true);
|
|
35
|
+
expect(fs.readFileSync(path.join(tempDir, target), 'utf-8')).toBe(
|
|
36
|
+
'KEY=value',
|
|
37
|
+
);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('skips copying when the target exists unless forced', () => {
|
|
41
|
+
const example = '.env.example';
|
|
42
|
+
const target = '.env';
|
|
43
|
+
fs.writeFileSync(path.join(tempDir, example), 'NEW=value');
|
|
44
|
+
fs.writeFileSync(path.join(tempDir, target), 'OLD=value');
|
|
45
|
+
const logSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
46
|
+
const skipped = bootstrapEnvFile(tempDir, example, target, {
|
|
47
|
+
quiet: false,
|
|
48
|
+
});
|
|
49
|
+
expect(skipped).toBe(false);
|
|
50
|
+
expect(fs.readFileSync(path.join(tempDir, target), 'utf-8')).toBe(
|
|
51
|
+
'OLD=value',
|
|
52
|
+
);
|
|
53
|
+
expect(logSpy).toHaveBeenCalled();
|
|
54
|
+
|
|
55
|
+
const forced = bootstrapEnvFile(tempDir, example, target, { force: true });
|
|
56
|
+
expect(forced).toBe(true);
|
|
57
|
+
expect(fs.readFileSync(path.join(tempDir, target), 'utf-8')).toBe(
|
|
58
|
+
'NEW=value',
|
|
59
|
+
);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('lists env file statuses using the provided config', () => {
|
|
63
|
+
const config = {
|
|
64
|
+
env: {
|
|
65
|
+
local: '.env',
|
|
66
|
+
localExample: '.env.example',
|
|
67
|
+
docker: '.env.docker',
|
|
68
|
+
dockerExample: '.env.docker.example',
|
|
69
|
+
},
|
|
70
|
+
} as const;
|
|
71
|
+
fs.writeFileSync(path.join(tempDir, '.env.example'), '');
|
|
72
|
+
fs.writeFileSync(path.join(tempDir, '.env'), '');
|
|
73
|
+
|
|
74
|
+
const statuses = listEnvStatus(tempDir, config as any);
|
|
75
|
+
|
|
76
|
+
expect(statuses).toEqual([
|
|
77
|
+
expect.objectContaining({ file: '.env.example', exists: true }),
|
|
78
|
+
expect.objectContaining({ file: '.env', exists: true }),
|
|
79
|
+
expect.objectContaining({ file: '.env.docker.example', exists: false }),
|
|
80
|
+
expect.objectContaining({ file: '.env.docker', exists: false }),
|
|
81
|
+
]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('resolves env example files relative to the project root', () => {
|
|
85
|
+
const envFile = '.env.local';
|
|
86
|
+
const defaultExample = '.env.example';
|
|
87
|
+
const examplePath = path.join(tempDir, `${envFile}.example`);
|
|
88
|
+
fs.writeFileSync(examplePath, '');
|
|
89
|
+
|
|
90
|
+
expect(resolveEnvExample(tempDir, envFile, defaultExample)).toBe(
|
|
91
|
+
`${envFile}.example`,
|
|
92
|
+
);
|
|
93
|
+
expect(resolveEnvExample(tempDir, '.missing', defaultExample)).toBe(
|
|
94
|
+
defaultExample,
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
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 fs from 'fs';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
|
|
11
|
+
import { jest } from '@jest/globals';
|
|
12
|
+
|
|
13
|
+
const readPackageJson = jest.fn();
|
|
14
|
+
const execSync = jest.fn();
|
|
15
|
+
|
|
16
|
+
jest.unstable_mockModule('../project.js', () => ({
|
|
17
|
+
readPackageJson,
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.unstable_mockModule('child_process', () => ({
|
|
21
|
+
execSync,
|
|
22
|
+
}));
|
|
23
|
+
|
|
24
|
+
type PackageManagerModule = typeof import('../package-manager.js');
|
|
25
|
+
let normalizePackageManager: PackageManagerModule['normalizePackageManager'];
|
|
26
|
+
let detectPackageManager: PackageManagerModule['detectPackageManager'];
|
|
27
|
+
let runPackageScript: PackageManagerModule['runPackageScript'];
|
|
28
|
+
let installDependencies: PackageManagerModule['installDependencies'];
|
|
29
|
+
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
({
|
|
32
|
+
normalizePackageManager,
|
|
33
|
+
detectPackageManager,
|
|
34
|
+
runPackageScript,
|
|
35
|
+
installDependencies,
|
|
36
|
+
} = await import('../package-manager.js'));
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
afterEach(() => {
|
|
40
|
+
jest.resetAllMocks();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('normalizePackageManager', () => {
|
|
44
|
+
it('returns normalized package manager names', () => {
|
|
45
|
+
expect(normalizePackageManager('PNPM')).toBe('pnpm');
|
|
46
|
+
expect(normalizePackageManager(undefined)).toBeUndefined();
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('exits when the package manager is unsupported', () => {
|
|
50
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => {
|
|
51
|
+
throw new Error('exit:1');
|
|
52
|
+
}) as () => never);
|
|
53
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
54
|
+
|
|
55
|
+
expect(() => normalizePackageManager('foo')).toThrow('exit:1');
|
|
56
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('detectPackageManager', () => {
|
|
61
|
+
let tempDir: string;
|
|
62
|
+
|
|
63
|
+
beforeEach(() => {
|
|
64
|
+
tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hexabot-pm-'));
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
afterEach(() => {
|
|
68
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('detects the package manager based on lockfiles', () => {
|
|
72
|
+
fs.writeFileSync(path.join(tempDir, 'pnpm-lock.yaml'), '');
|
|
73
|
+
|
|
74
|
+
expect(detectPackageManager(tempDir)).toBe('pnpm');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('falls back to npm when no lockfiles are found', () => {
|
|
78
|
+
expect(detectPackageManager(tempDir)).toBe('npm');
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
describe('installDependencies & runPackageScript', () => {
|
|
83
|
+
const projectRoot = '/tmp/project';
|
|
84
|
+
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
readPackageJson.mockReturnValue({
|
|
87
|
+
scripts: {
|
|
88
|
+
build: 'tsc',
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('runs installation commands for the chosen package manager', () => {
|
|
94
|
+
installDependencies('yarn', projectRoot);
|
|
95
|
+
|
|
96
|
+
expect(execSync).toHaveBeenCalledWith('yarn install', {
|
|
97
|
+
cwd: projectRoot,
|
|
98
|
+
stdio: 'inherit',
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('runs package scripts after ensuring the script exists', () => {
|
|
103
|
+
runPackageScript('pnpm', 'build', projectRoot, ['--watch']);
|
|
104
|
+
|
|
105
|
+
expect(execSync).toHaveBeenCalledWith('pnpm run build -- --watch', {
|
|
106
|
+
cwd: projectRoot,
|
|
107
|
+
stdio: 'inherit',
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('exits when the script is missing from package.json', () => {
|
|
112
|
+
readPackageJson.mockReturnValue({ scripts: {} });
|
|
113
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation((() => {
|
|
114
|
+
throw new Error('exit:1');
|
|
115
|
+
}) as () => never);
|
|
116
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
117
|
+
|
|
118
|
+
expect(() => runPackageScript('npm', 'dev', projectRoot)).toThrow('exit:1');
|
|
119
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
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 fs from 'fs';
|
|
8
|
+
import * as os from 'os';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
|
|
11
|
+
import { jest } from '@jest/globals';
|
|
12
|
+
|
|
13
|
+
import { ensureDockerFolder, resolveDockerFolder } from '../project.js';
|
|
14
|
+
|
|
15
|
+
describe('project helpers', () => {
|
|
16
|
+
let tempDirs: string[] = [];
|
|
17
|
+
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
jest.restoreAllMocks();
|
|
20
|
+
tempDirs.forEach((dir) =>
|
|
21
|
+
fs.rmSync(dir, {
|
|
22
|
+
recursive: true,
|
|
23
|
+
force: true,
|
|
24
|
+
}),
|
|
25
|
+
);
|
|
26
|
+
tempDirs = [];
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const createTempDir = (withDockerFolder: boolean) => {
|
|
30
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hexabot-project-'));
|
|
31
|
+
tempDirs.push(dir);
|
|
32
|
+
if (withDockerFolder) {
|
|
33
|
+
fs.mkdirSync(path.join(dir, 'docker'));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return dir;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
it('resolves the docker folder relative to the working directory', () => {
|
|
40
|
+
jest.spyOn(process, 'cwd').mockReturnValue('/workspace/hexabot');
|
|
41
|
+
|
|
42
|
+
expect(resolveDockerFolder()).toBe(
|
|
43
|
+
path.resolve('/workspace/hexabot', './docker'),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('returns the folder path when it exists', () => {
|
|
48
|
+
const cwd = createTempDir(true);
|
|
49
|
+
jest.spyOn(process, 'cwd').mockReturnValue(cwd);
|
|
50
|
+
|
|
51
|
+
expect(ensureDockerFolder()).toBe(path.resolve(cwd, './docker'));
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('exits the process when the docker folder is missing', () => {
|
|
55
|
+
const cwd = createTempDir(false);
|
|
56
|
+
jest.spyOn(process, 'cwd').mockReturnValue(cwd);
|
|
57
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
58
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
59
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(((
|
|
60
|
+
code?: string | number | null | undefined,
|
|
61
|
+
) => {
|
|
62
|
+
throw new Error(`exit:${code}`);
|
|
63
|
+
}) as (code?: string | number | null | undefined) => never);
|
|
64
|
+
|
|
65
|
+
expect(() => ensureDockerFolder()).toThrow('exit:1');
|
|
66
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
67
|
+
});
|
|
68
|
+
});
|