@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,49 @@
|
|
|
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 { ensureDockerFolder, resolveDockerFolder } from '../project.js';
|
|
11
|
+
describe('project helpers', () => {
|
|
12
|
+
let tempDirs = [];
|
|
13
|
+
afterEach(() => {
|
|
14
|
+
jest.restoreAllMocks();
|
|
15
|
+
tempDirs.forEach((dir) => fs.rmSync(dir, {
|
|
16
|
+
recursive: true,
|
|
17
|
+
force: true,
|
|
18
|
+
}));
|
|
19
|
+
tempDirs = [];
|
|
20
|
+
});
|
|
21
|
+
const createTempDir = (withDockerFolder) => {
|
|
22
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hexabot-project-'));
|
|
23
|
+
tempDirs.push(dir);
|
|
24
|
+
if (withDockerFolder) {
|
|
25
|
+
fs.mkdirSync(path.join(dir, 'docker'));
|
|
26
|
+
}
|
|
27
|
+
return dir;
|
|
28
|
+
};
|
|
29
|
+
it('resolves the docker folder relative to the working directory', () => {
|
|
30
|
+
jest.spyOn(process, 'cwd').mockReturnValue('/workspace/hexabot');
|
|
31
|
+
expect(resolveDockerFolder()).toBe(path.resolve('/workspace/hexabot', './docker'));
|
|
32
|
+
});
|
|
33
|
+
it('returns the folder path when it exists', () => {
|
|
34
|
+
const cwd = createTempDir(true);
|
|
35
|
+
jest.spyOn(process, 'cwd').mockReturnValue(cwd);
|
|
36
|
+
expect(ensureDockerFolder()).toBe(path.resolve(cwd, './docker'));
|
|
37
|
+
});
|
|
38
|
+
it('exits the process when the docker folder is missing', () => {
|
|
39
|
+
const cwd = createTempDir(false);
|
|
40
|
+
jest.spyOn(process, 'cwd').mockReturnValue(cwd);
|
|
41
|
+
jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
42
|
+
jest.spyOn(console, 'log').mockImplementation(() => { });
|
|
43
|
+
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(((code) => {
|
|
44
|
+
throw new Error(`exit:${code}`);
|
|
45
|
+
}));
|
|
46
|
+
expect(() => ensureDockerFolder()).toThrow('exit:1');
|
|
47
|
+
expect(exitSpy).toHaveBeenCalledWith(1);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
const CONFIG_FILE_NAME = 'hexabot.config.json';
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
devScript: 'dev',
|
|
12
|
+
startScript: 'start',
|
|
13
|
+
docker: {
|
|
14
|
+
composeFile: 'docker/docker-compose.yml',
|
|
15
|
+
defaultServices: [],
|
|
16
|
+
},
|
|
17
|
+
env: {
|
|
18
|
+
local: '.env',
|
|
19
|
+
localExample: '.env.example',
|
|
20
|
+
docker: '.env.docker',
|
|
21
|
+
dockerExample: '.env.docker.example',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
export const resolveConfigPath = (projectRoot = process.cwd()) => {
|
|
25
|
+
return path.join(projectRoot, CONFIG_FILE_NAME);
|
|
26
|
+
};
|
|
27
|
+
export const loadProjectConfig = (projectRoot = process.cwd()) => {
|
|
28
|
+
const configPath = resolveConfigPath(projectRoot);
|
|
29
|
+
if (!fs.existsSync(configPath)) {
|
|
30
|
+
return { ...DEFAULT_CONFIG };
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
const raw = fs.readFileSync(configPath, 'utf-8');
|
|
34
|
+
const parsed = JSON.parse(raw);
|
|
35
|
+
return mergeConfig(DEFAULT_CONFIG, parsed);
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
console.warn(chalk.yellow('Invalid hexabot.config.json. Using defaults.'));
|
|
39
|
+
console.warn(chalk.yellow(error.message));
|
|
40
|
+
return { ...DEFAULT_CONFIG };
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
export const ensureProjectConfig = (projectRoot = process.cwd(), overrides = {}) => {
|
|
44
|
+
const configPath = resolveConfigPath(projectRoot);
|
|
45
|
+
if (!fs.existsSync(configPath)) {
|
|
46
|
+
const config = mergeConfig(DEFAULT_CONFIG, overrides);
|
|
47
|
+
writeProjectConfig(config, projectRoot);
|
|
48
|
+
return config;
|
|
49
|
+
}
|
|
50
|
+
if (Object.keys(overrides).length > 0) {
|
|
51
|
+
return updateProjectConfig(projectRoot, overrides);
|
|
52
|
+
}
|
|
53
|
+
return loadProjectConfig(projectRoot);
|
|
54
|
+
};
|
|
55
|
+
export const writeProjectConfig = (config, projectRoot = process.cwd()) => {
|
|
56
|
+
const configPath = resolveConfigPath(projectRoot);
|
|
57
|
+
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, 'utf-8');
|
|
58
|
+
};
|
|
59
|
+
export const updateProjectConfig = (projectRoot, overrides) => {
|
|
60
|
+
const existing = loadProjectConfig(projectRoot);
|
|
61
|
+
const next = mergeConfig(existing, overrides);
|
|
62
|
+
writeProjectConfig(next, projectRoot);
|
|
63
|
+
return next;
|
|
64
|
+
};
|
|
65
|
+
const mergeConfig = (base, overrides) => {
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
...overrides,
|
|
69
|
+
docker: {
|
|
70
|
+
...base.docker,
|
|
71
|
+
...(overrides.docker || {}),
|
|
72
|
+
},
|
|
73
|
+
env: {
|
|
74
|
+
...base.env,
|
|
75
|
+
...(overrides.env || {}),
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
export const resolveComposeFile = (projectRoot, filePath) => {
|
|
11
|
+
return path.isAbsolute(filePath)
|
|
12
|
+
? filePath
|
|
13
|
+
: path.join(projectRoot, filePath);
|
|
14
|
+
};
|
|
15
|
+
export const generateComposeFiles = (baseComposeFile, services, mode) => {
|
|
16
|
+
if (!fs.existsSync(baseComposeFile)) {
|
|
17
|
+
console.error(chalk.red(`Docker compose file not found at ${baseComposeFile}. Update hexabot.config.json.`));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
20
|
+
const composeDir = path.dirname(baseComposeFile);
|
|
21
|
+
const files = [`-f ${baseComposeFile}`];
|
|
22
|
+
services.forEach((service) => {
|
|
23
|
+
const serviceFile = path.join(composeDir, `docker-compose.${service}.yml`);
|
|
24
|
+
if (fs.existsSync(serviceFile)) {
|
|
25
|
+
files.push(`-f ${serviceFile}`);
|
|
26
|
+
}
|
|
27
|
+
if (mode) {
|
|
28
|
+
const serviceModeFile = path.join(composeDir, `docker-compose.${service}.${mode}.yml`);
|
|
29
|
+
if (fs.existsSync(serviceModeFile)) {
|
|
30
|
+
files.push(`-f ${serviceModeFile}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
if (mode) {
|
|
35
|
+
const modeFile = path.join(composeDir, `docker-compose.${mode}.yml`);
|
|
36
|
+
if (fs.existsSync(modeFile)) {
|
|
37
|
+
files.push(`-f ${modeFile}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return files.join(' ');
|
|
41
|
+
};
|
|
42
|
+
export const dockerCompose = (args) => {
|
|
43
|
+
try {
|
|
44
|
+
const cmd = `docker compose ${args}`;
|
|
45
|
+
console.log(chalk.yellow(cmd));
|
|
46
|
+
execSync(cmd, { stdio: 'inherit' });
|
|
47
|
+
}
|
|
48
|
+
catch (_error) {
|
|
49
|
+
console.error(chalk.red('Error executing Docker Compose command.'));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
export const dockerExec = (container, command, options) => {
|
|
54
|
+
try {
|
|
55
|
+
const optionalArgs = options ? `${options} ` : '';
|
|
56
|
+
const cmd = `docker exec -it ${optionalArgs}${container} ${command}`;
|
|
57
|
+
console.log(chalk.bgYellow(cmd));
|
|
58
|
+
execSync(cmd, {
|
|
59
|
+
stdio: 'inherit',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (_error) {
|
|
63
|
+
console.error(chalk.red('Error executing Docker Exec command.'));
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
};
|
package/dist/core/env.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
export const bootstrapEnvFile = (projectRoot, exampleFile, targetFile, options = {}) => {
|
|
10
|
+
const examplePath = path.join(projectRoot, exampleFile);
|
|
11
|
+
const targetPath = path.join(projectRoot, targetFile);
|
|
12
|
+
if (!fs.existsSync(examplePath)) {
|
|
13
|
+
console.log(chalk.yellow(`Example env file "${exampleFile}" is missing. Skipping bootstrap.`));
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (!options.force && fs.existsSync(targetPath)) {
|
|
17
|
+
if (!options.quiet) {
|
|
18
|
+
console.log(chalk.gray(`Env file "${targetFile}" already exists. Use --force to overwrite.`));
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
fs.copyFileSync(examplePath, targetPath);
|
|
23
|
+
if (!options.quiet) {
|
|
24
|
+
console.log(chalk.green(`Generated ${targetFile} from ${exampleFile}. Customize it!`));
|
|
25
|
+
}
|
|
26
|
+
return true;
|
|
27
|
+
};
|
|
28
|
+
export const listEnvStatus = (projectRoot, config) => {
|
|
29
|
+
const entries = [
|
|
30
|
+
config.env.localExample,
|
|
31
|
+
config.env.local,
|
|
32
|
+
config.env.dockerExample,
|
|
33
|
+
config.env.docker,
|
|
34
|
+
];
|
|
35
|
+
return entries.map((file) => ({
|
|
36
|
+
file,
|
|
37
|
+
path: path.join(projectRoot, file),
|
|
38
|
+
exists: fs.existsSync(path.join(projectRoot, file)),
|
|
39
|
+
}));
|
|
40
|
+
};
|
|
41
|
+
export const resolveEnvExample = (projectRoot, envFile, defaultExample) => {
|
|
42
|
+
if (!envFile) {
|
|
43
|
+
return defaultExample;
|
|
44
|
+
}
|
|
45
|
+
const candidate = `${envFile}.example`;
|
|
46
|
+
if (fs.existsSync(path.join(projectRoot, candidate))) {
|
|
47
|
+
return candidate;
|
|
48
|
+
}
|
|
49
|
+
return defaultExample;
|
|
50
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
import { readPackageJson } from './project.js';
|
|
11
|
+
const PACKAGE_MANAGER_LOCKFILES = {
|
|
12
|
+
npm: 'package-lock.json',
|
|
13
|
+
pnpm: 'pnpm-lock.yaml',
|
|
14
|
+
yarn: 'yarn.lock',
|
|
15
|
+
bun: 'bun.lockb',
|
|
16
|
+
};
|
|
17
|
+
export const normalizePackageManager = (value) => {
|
|
18
|
+
if (!value) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const normalized = value.toLowerCase();
|
|
22
|
+
if (!Object.keys(PACKAGE_MANAGER_LOCKFILES).includes(normalized)) {
|
|
23
|
+
console.error(chalk.red(`Unsupported package manager "${value}". Use npm, pnpm, yarn, or bun.`));
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
return normalized;
|
|
27
|
+
};
|
|
28
|
+
export const detectPackageManager = (projectRoot = process.cwd()) => {
|
|
29
|
+
const entries = Object.entries(PACKAGE_MANAGER_LOCKFILES);
|
|
30
|
+
for (const [pm, lockfile] of entries) {
|
|
31
|
+
if (fs.existsSync(path.join(projectRoot, lockfile))) {
|
|
32
|
+
return pm;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return 'npm';
|
|
36
|
+
};
|
|
37
|
+
export const installDependencies = (pm, projectRoot = process.cwd()) => {
|
|
38
|
+
const command = getInstallCommand(pm);
|
|
39
|
+
runCommand(command, projectRoot);
|
|
40
|
+
};
|
|
41
|
+
export const runPackageScript = (pm, script, projectRoot = process.cwd(), scriptArgs = []) => {
|
|
42
|
+
ensureScriptExists(script, projectRoot);
|
|
43
|
+
const command = getRunScriptCommand(pm, script, scriptArgs);
|
|
44
|
+
runCommand(command, projectRoot);
|
|
45
|
+
};
|
|
46
|
+
const ensureScriptExists = (script, projectRoot) => {
|
|
47
|
+
const packageJson = readPackageJson(projectRoot);
|
|
48
|
+
if (!packageJson?.scripts?.[script]) {
|
|
49
|
+
console.error(chalk.red(`Cannot find script "${script}" in package.json. Update hexabot.config.json or package.json scripts.`));
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const getInstallCommand = (pm) => {
|
|
54
|
+
switch (pm) {
|
|
55
|
+
case 'pnpm':
|
|
56
|
+
return 'pnpm install';
|
|
57
|
+
case 'yarn':
|
|
58
|
+
return 'yarn install';
|
|
59
|
+
case 'bun':
|
|
60
|
+
return 'bun install';
|
|
61
|
+
default:
|
|
62
|
+
return 'npm install';
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const getRunScriptCommand = (pm, script, args) => {
|
|
66
|
+
const joinedArgs = args.join(' ');
|
|
67
|
+
switch (pm) {
|
|
68
|
+
case 'pnpm':
|
|
69
|
+
return joinedArgs
|
|
70
|
+
? `pnpm run ${script} -- ${joinedArgs}`
|
|
71
|
+
: `pnpm run ${script}`;
|
|
72
|
+
case 'yarn':
|
|
73
|
+
return joinedArgs ? `yarn ${script} ${joinedArgs}` : `yarn ${script}`;
|
|
74
|
+
case 'bun':
|
|
75
|
+
return joinedArgs
|
|
76
|
+
? `bun run ${script} ${joinedArgs}`
|
|
77
|
+
: `bun run ${script}`;
|
|
78
|
+
default:
|
|
79
|
+
return joinedArgs
|
|
80
|
+
? `npm run ${script} -- ${joinedArgs}`
|
|
81
|
+
: `npm run ${script}`;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const runCommand = (command, projectRoot) => {
|
|
85
|
+
console.log(chalk.cyan(command));
|
|
86
|
+
execSync(command, { cwd: projectRoot, stdio: 'inherit' });
|
|
87
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import { execSync } from 'child_process';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
const REQUIRED_NODE_VERSION = '20.18.1';
|
|
9
|
+
export const checkPrerequisites = (options = {}) => {
|
|
10
|
+
checkNodeVersion(options);
|
|
11
|
+
if (options.docker) {
|
|
12
|
+
checkDocker(options);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
export const checkNodeVersion = (options = {}) => {
|
|
16
|
+
try {
|
|
17
|
+
const nodeVersion = execSync('node --version', {
|
|
18
|
+
encoding: 'utf-8',
|
|
19
|
+
}).trim();
|
|
20
|
+
const currentNodeVersion = normalizeVersion(nodeVersion);
|
|
21
|
+
if (compareVersions(currentNodeVersion, REQUIRED_NODE_VERSION) >= 0) {
|
|
22
|
+
const message = `Node.js ${nodeVersion} ✓`;
|
|
23
|
+
logSuccess(message, options);
|
|
24
|
+
return { ok: true, message };
|
|
25
|
+
}
|
|
26
|
+
const message = `Node.js version must be at least ${REQUIRED_NODE_VERSION}. Current version: ${nodeVersion}.`;
|
|
27
|
+
handleFailure(message, options);
|
|
28
|
+
return { ok: false, message };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
const message = "Node.js is not accessible or installed correctly. Install Node.js v20.18.1+ and ensure it's in your PATH.";
|
|
32
|
+
handleFailure(message, options, error);
|
|
33
|
+
return { ok: false, message, details: error.message };
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
export const checkDocker = (options = {}) => {
|
|
37
|
+
try {
|
|
38
|
+
const dockerVersion = execSync('docker --version', {
|
|
39
|
+
encoding: 'utf-8',
|
|
40
|
+
}).trim();
|
|
41
|
+
const message = `Docker is installed: ${dockerVersion}`;
|
|
42
|
+
logSuccess(message, options);
|
|
43
|
+
return { ok: true, message };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
const message = 'Docker is required for this command. Please install Docker.';
|
|
47
|
+
handleFailure(message, options, error);
|
|
48
|
+
return { ok: false, message, details: error.message };
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const normalizeVersion = (version) => {
|
|
52
|
+
return version.startsWith('v') ? version.slice(1) : version;
|
|
53
|
+
};
|
|
54
|
+
const compareVersions = (current, required) => {
|
|
55
|
+
const currentParts = current.split('.').map(Number);
|
|
56
|
+
const requiredParts = required.split('.').map(Number);
|
|
57
|
+
for (let i = 0; i < requiredParts.length; i++) {
|
|
58
|
+
if ((currentParts[i] || 0) > (requiredParts[i] || 0)) {
|
|
59
|
+
return 1;
|
|
60
|
+
}
|
|
61
|
+
else if ((currentParts[i] || 0) < (requiredParts[i] || 0)) {
|
|
62
|
+
return -1;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return 0;
|
|
66
|
+
};
|
|
67
|
+
const logSuccess = (message, options) => {
|
|
68
|
+
if (!options.silent) {
|
|
69
|
+
console.log(chalk.green(message));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
const handleFailure = (message, options, error) => {
|
|
73
|
+
console.error(chalk.red(message));
|
|
74
|
+
if (options.fatal !== false) {
|
|
75
|
+
if (error && options.silent) {
|
|
76
|
+
console.error(chalk.red(error.message));
|
|
77
|
+
}
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import chalk from 'chalk';
|
|
9
|
+
const DOCKER_FOLDER_NAME = 'docker';
|
|
10
|
+
const HEXABOT_PACKAGE = '@hexabot-ai/api';
|
|
11
|
+
export const resolveProjectRoot = (cwd = process.cwd()) => {
|
|
12
|
+
return path.resolve(cwd);
|
|
13
|
+
};
|
|
14
|
+
export const readPackageJson = (projectRoot = process.cwd()) => {
|
|
15
|
+
try {
|
|
16
|
+
const packageJsonPath = path.join(projectRoot, 'package.json');
|
|
17
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
21
|
+
}
|
|
22
|
+
catch (_error) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
export const isHexabotProject = (projectRoot = process.cwd()) => {
|
|
27
|
+
const packageJson = readPackageJson(projectRoot);
|
|
28
|
+
if (!packageJson) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
return hasHexabotDependency(packageJson);
|
|
32
|
+
};
|
|
33
|
+
export const assertHexabotProject = (projectRoot = process.cwd()) => {
|
|
34
|
+
if (!isHexabotProject(projectRoot)) {
|
|
35
|
+
console.error(chalk.red('This command must be executed inside a Hexabot project (missing @hexabot-ai/api in package.json).'));
|
|
36
|
+
console.log(chalk.yellow('Run `hexabot create <project>` or navigate to your project.'));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export const resolveDockerFolder = (projectRoot = process.cwd()) => {
|
|
41
|
+
return path.resolve(projectRoot, `./${DOCKER_FOLDER_NAME}`);
|
|
42
|
+
};
|
|
43
|
+
export const ensureDockerFolder = (projectRoot = process.cwd()) => {
|
|
44
|
+
const folder = resolveDockerFolder(projectRoot);
|
|
45
|
+
if (!fs.existsSync(folder)) {
|
|
46
|
+
console.error(chalk.red(`The '${DOCKER_FOLDER_NAME}' folder is missing. Docker commands require the Hexabot project's docker/ directory.`));
|
|
47
|
+
console.log(chalk.yellow(`Ensure you're at the project root (e.g. cd path/to/my-bot) before running Docker commands.`));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
return folder;
|
|
51
|
+
};
|
|
52
|
+
const hasHexabotDependency = (packageJson) => {
|
|
53
|
+
const allDeps = {
|
|
54
|
+
...(packageJson.dependencies || {}),
|
|
55
|
+
...(packageJson.devDependencies || {}),
|
|
56
|
+
};
|
|
57
|
+
return Boolean(allDeps[HEXABOT_PACKAGE]);
|
|
58
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/*
|
|
3
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
4
|
+
* Copyright (c) 2025 Hexastack.
|
|
5
|
+
* Full terms: see LICENSE.md.
|
|
6
|
+
*/
|
|
7
|
+
import { createCliProgram } from './cli.js';
|
|
8
|
+
import { checkPrerequisites } from './core/prerequisites.js';
|
|
9
|
+
import { printBanner } from './ui/banner.js';
|
|
10
|
+
printBanner();
|
|
11
|
+
checkPrerequisites({ silent: true });
|
|
12
|
+
const program = createCliProgram();
|
|
13
|
+
program.parse(process.argv);
|
|
14
|
+
if (!process.argv.slice(2).length) {
|
|
15
|
+
program.outputHelp();
|
|
16
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import axios from 'axios';
|
|
9
|
+
import decompress from 'decompress';
|
|
10
|
+
export const downloadAndExtractTemplate = async (templateUrl, destination) => {
|
|
11
|
+
try {
|
|
12
|
+
const response = await axios({
|
|
13
|
+
url: templateUrl,
|
|
14
|
+
method: 'GET',
|
|
15
|
+
responseType: 'arraybuffer',
|
|
16
|
+
});
|
|
17
|
+
const zipFilePath = path.join(destination, 'template.zip');
|
|
18
|
+
fs.writeFileSync(zipFilePath, response.data);
|
|
19
|
+
await decompress(zipFilePath, destination, {
|
|
20
|
+
strip: 1,
|
|
21
|
+
});
|
|
22
|
+
fs.unlinkSync(zipFilePath);
|
|
23
|
+
}
|
|
24
|
+
catch (_error) {
|
|
25
|
+
throw new Error(`Failed to download template from GitHub`);
|
|
26
|
+
}
|
|
27
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import figlet from 'figlet';
|
|
8
|
+
import { getCliVersion } from '../utils/version.js';
|
|
9
|
+
export const printBanner = () => {
|
|
10
|
+
const bannerText = chalk.blue(figlet.textSync('Hexabot'));
|
|
11
|
+
const versionText = chalk.gray(`CLI v${getCliVersion()}`);
|
|
12
|
+
console.log(bannerText);
|
|
13
|
+
console.log(versionText);
|
|
14
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import { parseServices } from '../services.js';
|
|
7
|
+
describe('parseServices', () => {
|
|
8
|
+
it('splits and trims comma separated values', () => {
|
|
9
|
+
expect(parseServices('api, postgres,worker')).toEqual([
|
|
10
|
+
'api',
|
|
11
|
+
'postgres',
|
|
12
|
+
'worker',
|
|
13
|
+
]);
|
|
14
|
+
});
|
|
15
|
+
it('filters out empty service values', () => {
|
|
16
|
+
expect(parseServices(' , , ')).toEqual([]);
|
|
17
|
+
});
|
|
18
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
import { validateProjectName } from '../validation.js';
|
|
7
|
+
describe('validateProjectName', () => {
|
|
8
|
+
it('accepts lowercase alphanumeric names with dashes', () => {
|
|
9
|
+
expect(validateProjectName('hexabot-bot01')).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
it('rejects names that do not start with a letter', () => {
|
|
12
|
+
expect(validateProjectName('1hexabot')).toBe(false);
|
|
13
|
+
});
|
|
14
|
+
it('rejects names containing uppercase characters or spaces', () => {
|
|
15
|
+
expect(validateProjectName('Hexabot Agent')).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
@@ -0,0 +1,27 @@
|
|
|
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 path from 'path';
|
|
8
|
+
import { jest } from '@jest/globals';
|
|
9
|
+
import { getCliVersion } from '../version.js';
|
|
10
|
+
describe('getCliVersion', () => {
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
jest.restoreAllMocks();
|
|
13
|
+
});
|
|
14
|
+
it('returns the version from the package.json file', () => {
|
|
15
|
+
const packageJsonPath = path.resolve(process.cwd(), 'package.json');
|
|
16
|
+
const { version } = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
17
|
+
expect(getCliVersion()).toBe(version);
|
|
18
|
+
});
|
|
19
|
+
it('falls back to the default version when package.json cannot be read', () => {
|
|
20
|
+
const errorSpy = jest.spyOn(console, 'error').mockImplementation(() => { });
|
|
21
|
+
const failingReader = () => {
|
|
22
|
+
throw new Error('file error');
|
|
23
|
+
};
|
|
24
|
+
expect(getCliVersion(failingReader)).toBe('3.0.0');
|
|
25
|
+
expect(errorSpy).toHaveBeenCalled();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Hexabot — Fair Core License (FCL-1.0-ALv2)
|
|
3
|
+
* Copyright (c) 2025 Hexastack.
|
|
4
|
+
* Full terms: see LICENSE.md.
|
|
5
|
+
*/
|
|
6
|
+
export const parseServices = (serviceString) => {
|
|
7
|
+
return serviceString
|
|
8
|
+
.split(',')
|
|
9
|
+
.map((service) => service.trim())
|
|
10
|
+
.filter((s) => s);
|
|
11
|
+
};
|
|
@@ -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 * as fs from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
9
|
+
const INITIAL_CLI_VERSION = '3.0.0';
|
|
10
|
+
export const getCliVersion = (readFile = fs.readFileSync) => {
|
|
11
|
+
try {
|
|
12
|
+
const filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(filename);
|
|
14
|
+
const packageJsonPath = path.join(__dirname, '../../package.json');
|
|
15
|
+
const packageJson = JSON.parse(readFile(packageJsonPath, 'utf-8'));
|
|
16
|
+
return packageJson.version;
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
console.error('Error reading package.json:', error);
|
|
20
|
+
return INITIAL_CLI_VERSION;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
const baseConfig = require('./eslint.config.cjs');
|
|
2
|
+
|
|
3
|
+
const createConfig =
|
|
4
|
+
typeof baseConfig.createConfig === 'function'
|
|
5
|
+
? baseConfig.createConfig
|
|
6
|
+
: () => baseConfig;
|
|
7
|
+
|
|
8
|
+
module.exports = createConfig({
|
|
9
|
+
headerYear: String(new Date().getFullYear()),
|
|
10
|
+
});
|