chainlesschain 0.37.6
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/README.md +182 -0
- package/bin/chainlesschain.js +6 -0
- package/package.json +53 -0
- package/src/commands/config.js +105 -0
- package/src/commands/doctor.js +178 -0
- package/src/commands/services.js +94 -0
- package/src/commands/setup.js +193 -0
- package/src/commands/start.js +68 -0
- package/src/commands/status.js +105 -0
- package/src/commands/stop.js +49 -0
- package/src/commands/update.js +78 -0
- package/src/constants.js +112 -0
- package/src/index.js +34 -0
- package/src/lib/checksum.js +20 -0
- package/src/lib/config-manager.js +94 -0
- package/src/lib/downloader.js +122 -0
- package/src/lib/logger.js +63 -0
- package/src/lib/paths.js +80 -0
- package/src/lib/platform.js +53 -0
- package/src/lib/process-manager.js +128 -0
- package/src/lib/prompts.js +17 -0
- package/src/lib/service-manager.js +123 -0
- package/src/lib/version-checker.js +72 -0
package/README.md
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# chainlesschain CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for installing, configuring, and managing [ChainlessChain](https://www.chainlesschain.com) — a decentralized personal AI management system with hardware-level security.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g chainlesschain
|
|
9
|
+
chainlesschain setup
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Requirements
|
|
13
|
+
|
|
14
|
+
- **Node.js** >= 22.12.0
|
|
15
|
+
- **Docker** (optional, for backend services)
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### `chainlesschain setup`
|
|
20
|
+
|
|
21
|
+
Interactive setup wizard. Checks prerequisites, configures LLM provider, downloads the desktop binary, and optionally starts Docker services.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
chainlesschain setup
|
|
25
|
+
chainlesschain setup --skip-download # Skip binary download
|
|
26
|
+
chainlesschain setup --skip-services # Skip Docker setup
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### `chainlesschain start`
|
|
30
|
+
|
|
31
|
+
Launch the ChainlessChain desktop application.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
chainlesschain start # Launch GUI app
|
|
35
|
+
chainlesschain start --headless # Start backend services only (no GUI)
|
|
36
|
+
chainlesschain start --services # Also start Docker services
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### `chainlesschain stop`
|
|
40
|
+
|
|
41
|
+
Stop ChainlessChain.
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
chainlesschain stop # Stop desktop app
|
|
45
|
+
chainlesschain stop --services # Stop Docker services only
|
|
46
|
+
chainlesschain stop --all # Stop app + Docker services
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### `chainlesschain status`
|
|
50
|
+
|
|
51
|
+
Show status of the desktop app, Docker services, and port availability.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
chainlesschain status
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### `chainlesschain services <action>`
|
|
58
|
+
|
|
59
|
+
Manage Docker backend services (Ollama, Qdrant, PostgreSQL, Redis, etc.).
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
chainlesschain services up # Start all services
|
|
63
|
+
chainlesschain services up ollama redis # Start specific services
|
|
64
|
+
chainlesschain services down # Stop all services
|
|
65
|
+
chainlesschain services logs # View logs
|
|
66
|
+
chainlesschain services logs -f # Follow logs
|
|
67
|
+
chainlesschain services pull # Pull latest images
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### `chainlesschain config <action>`
|
|
71
|
+
|
|
72
|
+
Manage configuration.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
chainlesschain config list # Show all config values
|
|
76
|
+
chainlesschain config get llm.provider # Get a specific value
|
|
77
|
+
chainlesschain config set llm.provider openai
|
|
78
|
+
chainlesschain config set llm.apiKey sk-...
|
|
79
|
+
chainlesschain config edit # Open in $EDITOR
|
|
80
|
+
chainlesschain config reset # Reset to defaults
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### `chainlesschain update`
|
|
84
|
+
|
|
85
|
+
Check for and install updates.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
chainlesschain update # Update to latest stable
|
|
89
|
+
chainlesschain update --check # Check only, don't download
|
|
90
|
+
chainlesschain update --channel beta # Use beta channel
|
|
91
|
+
chainlesschain update --channel dev # Use dev channel
|
|
92
|
+
chainlesschain update --force # Re-download even if exists
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### `chainlesschain doctor`
|
|
96
|
+
|
|
97
|
+
Diagnose your environment.
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
chainlesschain doctor
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Checks: Node.js version, npm, Docker, Docker Compose, Git, config directory, binary installation, setup status, port availability, disk space.
|
|
104
|
+
|
|
105
|
+
## Global Options
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
chainlesschain --version # Show version
|
|
109
|
+
chainlesschain --help # Show help
|
|
110
|
+
chainlesschain --verbose # Enable verbose output
|
|
111
|
+
chainlesschain --quiet # Suppress non-essential output
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Configuration
|
|
115
|
+
|
|
116
|
+
Configuration is stored at `~/.chainlesschain/config.json`. The CLI creates and manages this file automatically during setup.
|
|
117
|
+
|
|
118
|
+
### Config Schema
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{
|
|
122
|
+
"setupCompleted": true,
|
|
123
|
+
"completedAt": "2026-03-11T00:00:00.000Z",
|
|
124
|
+
"edition": "personal",
|
|
125
|
+
"llm": {
|
|
126
|
+
"provider": "ollama",
|
|
127
|
+
"apiKey": null,
|
|
128
|
+
"baseUrl": "http://localhost:11434",
|
|
129
|
+
"model": "qwen2:7b"
|
|
130
|
+
},
|
|
131
|
+
"enterprise": {
|
|
132
|
+
"serverUrl": null,
|
|
133
|
+
"apiKey": null,
|
|
134
|
+
"tenantId": null
|
|
135
|
+
},
|
|
136
|
+
"services": {
|
|
137
|
+
"autoStart": false,
|
|
138
|
+
"dockerComposePath": null
|
|
139
|
+
},
|
|
140
|
+
"update": {
|
|
141
|
+
"channel": "stable",
|
|
142
|
+
"autoCheck": true
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### Supported LLM Providers
|
|
148
|
+
|
|
149
|
+
| Provider | Default Model | API Key Required |
|
|
150
|
+
| ------------------- | ------------- | ---------------- |
|
|
151
|
+
| Ollama (Local) | qwen2:7b | No |
|
|
152
|
+
| OpenAI | gpt-4o | Yes |
|
|
153
|
+
| DashScope (Alibaba) | qwen-max | Yes |
|
|
154
|
+
| DeepSeek | deepseek-chat | Yes |
|
|
155
|
+
| Custom | — | Yes |
|
|
156
|
+
|
|
157
|
+
## File Structure
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
~/.chainlesschain/
|
|
161
|
+
├── config.json # Configuration
|
|
162
|
+
├── bin/ # Downloaded binaries
|
|
163
|
+
├── state/ # Runtime state (PID files)
|
|
164
|
+
├── services/ # Service configurations
|
|
165
|
+
├── logs/ # CLI logs
|
|
166
|
+
└── cache/ # Download cache
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Development
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
cd packages/cli
|
|
173
|
+
npm install
|
|
174
|
+
npm test # Run all tests
|
|
175
|
+
npm run test:unit # Unit tests only
|
|
176
|
+
npm run test:integration # Integration tests
|
|
177
|
+
npm run test:e2e # End-to-end tests
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chainlesschain",
|
|
3
|
+
"version": "0.37.6",
|
|
4
|
+
"description": "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"chainlesschain": "./bin/chainlesschain.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "src/index.js",
|
|
10
|
+
"scripts": {
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:unit": "vitest run __tests__/unit/",
|
|
13
|
+
"test:integration": "vitest run __tests__/integration/",
|
|
14
|
+
"test:e2e": "vitest run __tests__/e2e/",
|
|
15
|
+
"test:watch": "vitest watch",
|
|
16
|
+
"lint": "eslint src/ bin/ --ext .js",
|
|
17
|
+
"format": "prettier --write \"src/**/*.js\" \"bin/**/*.js\" \"__tests__/**/*.js\""
|
|
18
|
+
},
|
|
19
|
+
"engines": {
|
|
20
|
+
"node": ">=22.12.0"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"bin/",
|
|
24
|
+
"src/",
|
|
25
|
+
"README.md"
|
|
26
|
+
],
|
|
27
|
+
"keywords": [
|
|
28
|
+
"chainlesschain",
|
|
29
|
+
"ai",
|
|
30
|
+
"cli",
|
|
31
|
+
"electron",
|
|
32
|
+
"decentralized",
|
|
33
|
+
"knowledge-base"
|
|
34
|
+
],
|
|
35
|
+
"author": "ChainlessChain Team",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/yourname/chainlesschain.git",
|
|
40
|
+
"directory": "packages/cli"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://www.chainlesschain.com",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"commander": "^12.1.0",
|
|
45
|
+
"@inquirer/prompts": "^7.2.0",
|
|
46
|
+
"chalk": "^5.4.1",
|
|
47
|
+
"ora": "^8.1.1",
|
|
48
|
+
"semver": "^7.6.3"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"vitest": "^3.1.1"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import {
|
|
3
|
+
loadConfig,
|
|
4
|
+
getConfigValue,
|
|
5
|
+
setConfigValue,
|
|
6
|
+
resetConfig,
|
|
7
|
+
saveConfig,
|
|
8
|
+
} from "../lib/config-manager.js";
|
|
9
|
+
import { getConfigPath } from "../lib/paths.js";
|
|
10
|
+
import logger from "../lib/logger.js";
|
|
11
|
+
|
|
12
|
+
export function registerConfigCommand(program) {
|
|
13
|
+
const cmd = program
|
|
14
|
+
.command("config")
|
|
15
|
+
.description("Manage ChainlessChain configuration");
|
|
16
|
+
|
|
17
|
+
cmd
|
|
18
|
+
.command("list")
|
|
19
|
+
.description("Show all configuration values")
|
|
20
|
+
.action(() => {
|
|
21
|
+
const config = loadConfig();
|
|
22
|
+
logger.log(chalk.bold(`\n Config: ${getConfigPath()}\n`));
|
|
23
|
+
printConfig(config, " ");
|
|
24
|
+
logger.newline();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
cmd
|
|
28
|
+
.command("get")
|
|
29
|
+
.description("Get a configuration value")
|
|
30
|
+
.argument("<key>", "Config key (dot-notation, e.g. llm.provider)")
|
|
31
|
+
.action((key) => {
|
|
32
|
+
const value = getConfigValue(key);
|
|
33
|
+
if (value === undefined) {
|
|
34
|
+
logger.error(`Key not found: ${key}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
if (typeof value === "object") {
|
|
38
|
+
logger.log(JSON.stringify(value, null, 2));
|
|
39
|
+
} else {
|
|
40
|
+
logger.log(String(value));
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
cmd
|
|
45
|
+
.command("set")
|
|
46
|
+
.description("Set a configuration value")
|
|
47
|
+
.argument("<key>", "Config key (dot-notation)")
|
|
48
|
+
.argument("<value>", "Value to set")
|
|
49
|
+
.action((key, value) => {
|
|
50
|
+
setConfigValue(key, value);
|
|
51
|
+
logger.success(`Set ${key} = ${value}`);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
cmd
|
|
55
|
+
.command("edit")
|
|
56
|
+
.description("Open config file in default editor")
|
|
57
|
+
.action(async () => {
|
|
58
|
+
const configPath = getConfigPath();
|
|
59
|
+
const editor =
|
|
60
|
+
process.env.EDITOR ||
|
|
61
|
+
process.env.VISUAL ||
|
|
62
|
+
(process.platform === "win32" ? "notepad" : "vi");
|
|
63
|
+
const { execSync } = await import("node:child_process");
|
|
64
|
+
try {
|
|
65
|
+
execSync(`${editor} "${configPath}"`, { stdio: "inherit" });
|
|
66
|
+
} catch (err) {
|
|
67
|
+
logger.error(`Failed to open editor: ${err.message}`);
|
|
68
|
+
logger.info(`Config file is at: ${configPath}`);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
cmd
|
|
73
|
+
.command("reset")
|
|
74
|
+
.description("Reset configuration to defaults")
|
|
75
|
+
.action(async () => {
|
|
76
|
+
const { askConfirm } = await import("../lib/prompts.js");
|
|
77
|
+
const confirmed = await askConfirm(
|
|
78
|
+
"Reset all configuration to defaults?",
|
|
79
|
+
false,
|
|
80
|
+
);
|
|
81
|
+
if (confirmed) {
|
|
82
|
+
resetConfig();
|
|
83
|
+
logger.success("Configuration reset to defaults");
|
|
84
|
+
} else {
|
|
85
|
+
logger.info("Reset cancelled");
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function printConfig(obj, indent = "") {
|
|
91
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
92
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
93
|
+
logger.log(`${indent}${chalk.cyan(key)}:`);
|
|
94
|
+
printConfig(value, indent + " ");
|
|
95
|
+
} else {
|
|
96
|
+
const displayValue =
|
|
97
|
+
value === null
|
|
98
|
+
? chalk.gray("null")
|
|
99
|
+
: key.toLowerCase().includes("key") && value
|
|
100
|
+
? chalk.yellow("****")
|
|
101
|
+
: String(value);
|
|
102
|
+
logger.log(`${indent}${chalk.cyan(key)}: ${displayValue}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
4
|
+
import { createConnection } from "node:net";
|
|
5
|
+
import semver from "semver";
|
|
6
|
+
import { MIN_NODE_VERSION, DEFAULT_PORTS, VERSION } from "../constants.js";
|
|
7
|
+
import { getHomeDir, getConfigPath, getBinDir } from "../lib/paths.js";
|
|
8
|
+
import {
|
|
9
|
+
isDockerAvailable,
|
|
10
|
+
isDockerComposeAvailable,
|
|
11
|
+
} from "../lib/service-manager.js";
|
|
12
|
+
import { loadConfig } from "../lib/config-manager.js";
|
|
13
|
+
import logger from "../lib/logger.js";
|
|
14
|
+
|
|
15
|
+
export function registerDoctorCommand(program) {
|
|
16
|
+
program
|
|
17
|
+
.command("doctor")
|
|
18
|
+
.description("Diagnose your ChainlessChain environment")
|
|
19
|
+
.action(async () => {
|
|
20
|
+
logger.log(chalk.bold("\n ChainlessChain Doctor\n"));
|
|
21
|
+
|
|
22
|
+
const checks = [];
|
|
23
|
+
|
|
24
|
+
// Node.js
|
|
25
|
+
const nodeVersion = process.versions.node;
|
|
26
|
+
const nodeOk = semver.gte(nodeVersion, MIN_NODE_VERSION);
|
|
27
|
+
checks.push({
|
|
28
|
+
name: `Node.js ${nodeVersion}`,
|
|
29
|
+
ok: nodeOk,
|
|
30
|
+
detail: nodeOk ? "" : `Requires >=${MIN_NODE_VERSION}`,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// npm
|
|
34
|
+
try {
|
|
35
|
+
const npmVersion = execSync("npm --version", {
|
|
36
|
+
encoding: "utf-8",
|
|
37
|
+
}).trim();
|
|
38
|
+
checks.push({ name: `npm ${npmVersion}`, ok: true });
|
|
39
|
+
} catch {
|
|
40
|
+
checks.push({ name: "npm", ok: false, detail: "Not found" });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Docker
|
|
44
|
+
checks.push({
|
|
45
|
+
name: "Docker",
|
|
46
|
+
ok: isDockerAvailable(),
|
|
47
|
+
detail: isDockerAvailable() ? "" : "Not installed (optional)",
|
|
48
|
+
});
|
|
49
|
+
checks.push({
|
|
50
|
+
name: "Docker Compose",
|
|
51
|
+
ok: isDockerComposeAvailable(),
|
|
52
|
+
detail: isDockerComposeAvailable() ? "" : "Not installed (optional)",
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Git
|
|
56
|
+
try {
|
|
57
|
+
const gitVersion = execSync("git --version", {
|
|
58
|
+
encoding: "utf-8",
|
|
59
|
+
}).trim();
|
|
60
|
+
checks.push({ name: gitVersion, ok: true });
|
|
61
|
+
} catch {
|
|
62
|
+
checks.push({ name: "Git", ok: false, detail: "Not found" });
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Config directory
|
|
66
|
+
const homeDir = getHomeDir();
|
|
67
|
+
checks.push({
|
|
68
|
+
name: `Config dir: ${homeDir}`,
|
|
69
|
+
ok: existsSync(homeDir),
|
|
70
|
+
detail: existsSync(homeDir) ? "" : 'Run "chainlesschain setup"',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Config file
|
|
74
|
+
const configPath = getConfigPath();
|
|
75
|
+
checks.push({
|
|
76
|
+
name: "Config file",
|
|
77
|
+
ok: existsSync(configPath),
|
|
78
|
+
detail: existsSync(configPath) ? "" : 'Run "chainlesschain setup"',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// Binary
|
|
82
|
+
const binDir = getBinDir();
|
|
83
|
+
const hasBin = existsSync(binDir) && readdirSafe(binDir).length > 0;
|
|
84
|
+
checks.push({
|
|
85
|
+
name: "Desktop binary",
|
|
86
|
+
ok: hasBin,
|
|
87
|
+
detail: hasBin
|
|
88
|
+
? ""
|
|
89
|
+
: 'Run "chainlesschain setup" or "chainlesschain update"',
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Setup completed
|
|
93
|
+
const config = loadConfig();
|
|
94
|
+
checks.push({
|
|
95
|
+
name: "Setup completed",
|
|
96
|
+
ok: config.setupCompleted,
|
|
97
|
+
detail: config.setupCompleted ? "" : 'Run "chainlesschain setup"',
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Print results
|
|
101
|
+
for (const check of checks) {
|
|
102
|
+
const icon = check.ok ? chalk.green("✔") : chalk.red("✖");
|
|
103
|
+
const detail = check.detail ? chalk.gray(` (${check.detail})`) : "";
|
|
104
|
+
logger.log(` ${icon} ${check.name}${detail}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Port scan
|
|
108
|
+
logger.log(chalk.bold("\n Port Status\n"));
|
|
109
|
+
for (const [name, port] of Object.entries(DEFAULT_PORTS)) {
|
|
110
|
+
const open = await checkPort(port);
|
|
111
|
+
const icon = open ? chalk.green("●") : chalk.gray("○");
|
|
112
|
+
logger.log(` ${icon} ${name}: ${port}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Disk space (basic)
|
|
116
|
+
try {
|
|
117
|
+
const { statfsSync } = await import("node:fs");
|
|
118
|
+
// statfsSync available in Node 22+
|
|
119
|
+
if (statfsSync) {
|
|
120
|
+
const stats = statfsSync(homeDir);
|
|
121
|
+
const freeGB = (stats.bavail * stats.bsize) / (1024 * 1024 * 1024);
|
|
122
|
+
const ok = freeGB > 2;
|
|
123
|
+
logger.log(chalk.bold("\n Disk\n"));
|
|
124
|
+
const icon = ok ? chalk.green("✔") : chalk.yellow("⚠");
|
|
125
|
+
logger.log(` ${icon} Free space: ${freeGB.toFixed(1)} GB`);
|
|
126
|
+
}
|
|
127
|
+
} catch {
|
|
128
|
+
// statfsSync not available on all platforms
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Summary
|
|
132
|
+
const failures = checks.filter((c) => !c.ok);
|
|
133
|
+
logger.newline();
|
|
134
|
+
if (failures.length === 0) {
|
|
135
|
+
logger.log(chalk.bold.green(" All checks passed!\n"));
|
|
136
|
+
} else {
|
|
137
|
+
const critical = failures.filter(
|
|
138
|
+
(c) => !c.detail?.includes("optional"),
|
|
139
|
+
);
|
|
140
|
+
if (critical.length > 0) {
|
|
141
|
+
logger.log(
|
|
142
|
+
chalk.bold.red(
|
|
143
|
+
` ${critical.length} issue(s) found. See details above.\n`,
|
|
144
|
+
),
|
|
145
|
+
);
|
|
146
|
+
} else {
|
|
147
|
+
logger.log(
|
|
148
|
+
chalk.bold.yellow(
|
|
149
|
+
` ${failures.length} optional component(s) missing.\n`,
|
|
150
|
+
),
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function checkPort(port, host = "127.0.0.1") {
|
|
158
|
+
return new Promise((resolve) => {
|
|
159
|
+
const socket = createConnection({ port, host, timeout: 1000 });
|
|
160
|
+
socket.on("connect", () => {
|
|
161
|
+
socket.destroy();
|
|
162
|
+
resolve(true);
|
|
163
|
+
});
|
|
164
|
+
socket.on("error", () => resolve(false));
|
|
165
|
+
socket.on("timeout", () => {
|
|
166
|
+
socket.destroy();
|
|
167
|
+
resolve(false);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function readdirSafe(dir) {
|
|
173
|
+
try {
|
|
174
|
+
return readdirSync(dir);
|
|
175
|
+
} catch {
|
|
176
|
+
return [];
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
isDockerAvailable,
|
|
3
|
+
isDockerComposeAvailable,
|
|
4
|
+
servicesUp,
|
|
5
|
+
servicesDown,
|
|
6
|
+
servicesLogs,
|
|
7
|
+
servicesPull,
|
|
8
|
+
findComposeFile,
|
|
9
|
+
} from "../lib/service-manager.js";
|
|
10
|
+
import logger from "../lib/logger.js";
|
|
11
|
+
|
|
12
|
+
export function registerServicesCommand(program) {
|
|
13
|
+
const cmd = program
|
|
14
|
+
.command("services")
|
|
15
|
+
.description("Manage Docker backend services");
|
|
16
|
+
|
|
17
|
+
cmd
|
|
18
|
+
.command("up")
|
|
19
|
+
.description("Start Docker services")
|
|
20
|
+
.argument("[services...]", "Specific services to start")
|
|
21
|
+
.action(async (services) => {
|
|
22
|
+
await withCompose((composePath) => {
|
|
23
|
+
logger.info("Starting services...");
|
|
24
|
+
servicesUp(composePath, {
|
|
25
|
+
services: services.length ? services : undefined,
|
|
26
|
+
});
|
|
27
|
+
logger.success("Services started");
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
cmd
|
|
32
|
+
.command("down")
|
|
33
|
+
.description("Stop Docker services")
|
|
34
|
+
.action(async () => {
|
|
35
|
+
await withCompose((composePath) => {
|
|
36
|
+
logger.info("Stopping services...");
|
|
37
|
+
servicesDown(composePath);
|
|
38
|
+
logger.success("Services stopped");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
cmd
|
|
43
|
+
.command("logs")
|
|
44
|
+
.description("View service logs")
|
|
45
|
+
.option("-f, --follow", "Follow log output")
|
|
46
|
+
.option("--tail <lines>", "Number of lines to show", "100")
|
|
47
|
+
.argument("[services...]", "Specific services")
|
|
48
|
+
.action(async (services, options) => {
|
|
49
|
+
await withCompose(async (composePath) => {
|
|
50
|
+
await servicesLogs(composePath, {
|
|
51
|
+
follow: options.follow,
|
|
52
|
+
tail: options.tail,
|
|
53
|
+
services: services.length ? services : undefined,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
cmd
|
|
59
|
+
.command("pull")
|
|
60
|
+
.description("Pull latest service images")
|
|
61
|
+
.action(async () => {
|
|
62
|
+
await withCompose((composePath) => {
|
|
63
|
+
logger.info("Pulling images...");
|
|
64
|
+
servicesPull(composePath);
|
|
65
|
+
logger.success("Images updated");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function withCompose(fn) {
|
|
71
|
+
try {
|
|
72
|
+
if (!isDockerAvailable()) {
|
|
73
|
+
logger.error("Docker is not installed or not running");
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
76
|
+
if (!isDockerComposeAvailable()) {
|
|
77
|
+
logger.error("Docker Compose is not available");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const composePath = findComposeFile([process.cwd(), "backend/docker"]);
|
|
82
|
+
if (!composePath) {
|
|
83
|
+
logger.error(
|
|
84
|
+
"docker-compose.yml not found. Run from the project root directory.",
|
|
85
|
+
);
|
|
86
|
+
process.exit(1);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
await fn(composePath);
|
|
90
|
+
} catch (err) {
|
|
91
|
+
logger.error(`Service operation failed: ${err.message}`);
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|