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.
@@ -0,0 +1,193 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import semver from "semver";
4
+ import {
5
+ MIN_NODE_VERSION,
6
+ LLM_PROVIDERS,
7
+ EDITIONS,
8
+ VERSION,
9
+ } from "../constants.js";
10
+ import { ensureHomeDir, getConfigPath } from "../lib/paths.js";
11
+ import { loadConfig, saveConfig } from "../lib/config-manager.js";
12
+ import {
13
+ isDockerAvailable,
14
+ isDockerComposeAvailable,
15
+ servicesUp,
16
+ findComposeFile,
17
+ } from "../lib/service-manager.js";
18
+ import { downloadRelease } from "../lib/downloader.js";
19
+ import {
20
+ askConfirm,
21
+ askSelect,
22
+ askInput,
23
+ askPassword,
24
+ } from "../lib/prompts.js";
25
+ import logger from "../lib/logger.js";
26
+
27
+ export function registerSetupCommand(program) {
28
+ program
29
+ .command("setup")
30
+ .description("Interactive setup wizard for ChainlessChain")
31
+ .option("--skip-download", "Skip binary download")
32
+ .option("--skip-services", "Skip Docker service setup")
33
+ .action(async (options) => {
34
+ try {
35
+ await runSetup(options);
36
+ } catch (err) {
37
+ if (err.name === "ExitPromptError") {
38
+ logger.log("\nSetup cancelled.");
39
+ process.exit(0);
40
+ }
41
+ logger.error(`Setup failed: ${err.message}`);
42
+ process.exit(1);
43
+ }
44
+ });
45
+ }
46
+
47
+ async function runSetup(options) {
48
+ logger.log(chalk.bold("\n ChainlessChain Setup Wizard\n"));
49
+ logger.log(` Version: ${VERSION}`);
50
+ logger.log(` Config: ${getConfigPath()}\n`);
51
+
52
+ // Step 1: Check Node version
53
+ const nodeVersion = process.versions.node;
54
+ if (!semver.gte(nodeVersion, MIN_NODE_VERSION)) {
55
+ logger.error(
56
+ `Node.js ${MIN_NODE_VERSION}+ required (current: ${nodeVersion})`,
57
+ );
58
+ process.exit(1);
59
+ }
60
+ logger.success(`Node.js ${nodeVersion}`);
61
+
62
+ // Step 2: Check Docker
63
+ const dockerOk = isDockerAvailable();
64
+ const composeOk = isDockerComposeAvailable();
65
+ if (dockerOk) {
66
+ logger.success("Docker available");
67
+ } else {
68
+ logger.warn("Docker not found (optional - needed for backend services)");
69
+ }
70
+ if (composeOk) {
71
+ logger.success("Docker Compose available");
72
+ }
73
+
74
+ // Step 3: Ensure home directory
75
+ ensureHomeDir();
76
+ logger.success("Configuration directory ready");
77
+ logger.newline();
78
+
79
+ // Step 4: Select edition
80
+ const edition = await askSelect("Select edition:", [
81
+ {
82
+ name: `${EDITIONS.personal.name} - ${EDITIONS.personal.description}`,
83
+ value: "personal",
84
+ },
85
+ {
86
+ name: `${EDITIONS.enterprise.name} - ${EDITIONS.enterprise.description}`,
87
+ value: "enterprise",
88
+ },
89
+ ]);
90
+
91
+ // Step 5: Configure LLM
92
+ const providerChoices = Object.entries(LLM_PROVIDERS).map(([key, info]) => ({
93
+ name: info.name,
94
+ value: key,
95
+ }));
96
+ const provider = await askSelect("Select LLM provider:", providerChoices);
97
+ const providerInfo = LLM_PROVIDERS[provider];
98
+
99
+ let apiKey = null;
100
+ let baseUrl = providerInfo.defaultBaseUrl;
101
+ let model = providerInfo.defaultModel;
102
+
103
+ if (providerInfo.requiresApiKey) {
104
+ apiKey = await askPassword(`Enter ${providerInfo.name} API key:`);
105
+ }
106
+
107
+ if (provider === "custom") {
108
+ baseUrl = await askInput("Enter API base URL:");
109
+ model = await askInput("Enter model name:");
110
+ } else {
111
+ const customizeModel = await askConfirm(
112
+ `Use default model (${model})?`,
113
+ true,
114
+ );
115
+ if (!customizeModel) {
116
+ model = await askInput("Enter model name:");
117
+ }
118
+ }
119
+
120
+ // Step 6: Download binary
121
+ if (!options.skipDownload) {
122
+ logger.newline();
123
+ const doDownload = await askConfirm(
124
+ "Download ChainlessChain desktop application?",
125
+ true,
126
+ );
127
+ if (doDownload) {
128
+ try {
129
+ await downloadRelease(VERSION);
130
+ } catch (err) {
131
+ logger.warn(`Download failed: ${err.message}`);
132
+ logger.info(
133
+ "You can download manually later with: chainlesschain update",
134
+ );
135
+ }
136
+ }
137
+ }
138
+
139
+ // Step 7: Save config
140
+ const config = loadConfig();
141
+ config.setupCompleted = true;
142
+ config.completedAt = new Date().toISOString();
143
+ config.edition = edition;
144
+ config.llm.provider = provider;
145
+ config.llm.apiKey = apiKey;
146
+ config.llm.baseUrl = baseUrl;
147
+ config.llm.model = model;
148
+ saveConfig(config);
149
+ logger.success("Configuration saved");
150
+
151
+ // Step 8: Docker services
152
+ if (!options.skipServices && dockerOk && composeOk) {
153
+ logger.newline();
154
+ const startServices = await askConfirm(
155
+ "Start Docker backend services?",
156
+ false,
157
+ );
158
+ if (startServices) {
159
+ const composePath = findComposeFile([process.cwd(), "backend/docker"]);
160
+ if (composePath) {
161
+ const spinner = ora("Starting services...").start();
162
+ try {
163
+ servicesUp(composePath);
164
+ spinner.succeed("Docker services started");
165
+ } catch (err) {
166
+ spinner.fail(`Failed to start services: ${err.message}`);
167
+ }
168
+ } else {
169
+ logger.warn(
170
+ 'docker-compose.yml not found. Run from the project root or use "chainlesschain services up".',
171
+ );
172
+ }
173
+ }
174
+ }
175
+
176
+ // Done
177
+ logger.newline();
178
+ logger.log(chalk.bold.green(" Setup complete!\n"));
179
+ logger.log(" Next steps:");
180
+ logger.log(
181
+ ` ${chalk.cyan("chainlesschain start")} Launch the desktop app`,
182
+ );
183
+ logger.log(
184
+ ` ${chalk.cyan("chainlesschain services up")} Start backend services`,
185
+ );
186
+ logger.log(
187
+ ` ${chalk.cyan("chainlesschain status")} Check system status`,
188
+ );
189
+ logger.log(
190
+ ` ${chalk.cyan("chainlesschain doctor")} Diagnose environment`,
191
+ );
192
+ logger.newline();
193
+ }
@@ -0,0 +1,68 @@
1
+ import chalk from "chalk";
2
+ import { startApp, isAppRunning } from "../lib/process-manager.js";
3
+ import {
4
+ servicesUp,
5
+ findComposeFile,
6
+ isDockerAvailable,
7
+ } from "../lib/service-manager.js";
8
+ import { loadConfig } from "../lib/config-manager.js";
9
+ import logger from "../lib/logger.js";
10
+
11
+ export function registerStartCommand(program) {
12
+ program
13
+ .command("start")
14
+ .description("Launch ChainlessChain desktop application")
15
+ .option("--headless", "Start backend services only (no GUI)")
16
+ .option("--services", "Also start Docker services")
17
+ .action(async (options) => {
18
+ try {
19
+ const config = loadConfig();
20
+
21
+ if (!config.setupCompleted) {
22
+ logger.warn('Setup not completed. Run "chainlesschain setup" first.');
23
+ process.exit(1);
24
+ }
25
+
26
+ if (options.services || options.headless) {
27
+ if (!isDockerAvailable()) {
28
+ logger.error(
29
+ "Docker is required for --headless and --services modes",
30
+ );
31
+ process.exit(1);
32
+ }
33
+ const composePath = findComposeFile([
34
+ process.cwd(),
35
+ "backend/docker",
36
+ ]);
37
+ if (composePath) {
38
+ logger.info("Starting Docker services...");
39
+ servicesUp(composePath);
40
+ logger.success("Docker services started");
41
+ } else {
42
+ logger.warn("docker-compose.yml not found");
43
+ }
44
+ }
45
+
46
+ if (!options.headless) {
47
+ if (isAppRunning()) {
48
+ logger.info("ChainlessChain is already running");
49
+ return;
50
+ }
51
+
52
+ logger.info("Starting ChainlessChain...");
53
+ const pid = startApp({ headless: false });
54
+
55
+ if (pid) {
56
+ logger.success(`ChainlessChain started (PID: ${pid})`);
57
+ } else {
58
+ logger.warn("App was already running");
59
+ }
60
+ } else {
61
+ logger.success("Backend services running in headless mode");
62
+ }
63
+ } catch (err) {
64
+ logger.error(`Failed to start: ${err.message}`);
65
+ process.exit(1);
66
+ }
67
+ });
68
+ }
@@ -0,0 +1,105 @@
1
+ import chalk from "chalk";
2
+ import { createConnection } from "node:net";
3
+ import { isAppRunning, getAppPid } from "../lib/process-manager.js";
4
+ import {
5
+ isDockerAvailable,
6
+ getServiceStatus,
7
+ findComposeFile,
8
+ } from "../lib/service-manager.js";
9
+ import { loadConfig } from "../lib/config-manager.js";
10
+ import { DEFAULT_PORTS } from "../constants.js";
11
+ import logger from "../lib/logger.js";
12
+
13
+ export function registerStatusCommand(program) {
14
+ program
15
+ .command("status")
16
+ .description("Show status of ChainlessChain app and services")
17
+ .action(async () => {
18
+ try {
19
+ const config = loadConfig();
20
+
21
+ // App status
22
+ logger.log(chalk.bold("\n App Status\n"));
23
+ if (isAppRunning()) {
24
+ const pid = getAppPid();
25
+ logger.log(` ${chalk.green("●")} Desktop app running (PID: ${pid})`);
26
+ } else {
27
+ logger.log(` ${chalk.gray("○")} Desktop app not running`);
28
+ }
29
+
30
+ // Setup status
31
+ if (config.setupCompleted) {
32
+ logger.log(
33
+ ` ${chalk.green("●")} Setup completed (${config.completedAt || "unknown"})`,
34
+ );
35
+ logger.log(` Edition: ${config.edition}`);
36
+ logger.log(` LLM: ${config.llm.provider} (${config.llm.model})`);
37
+ } else {
38
+ logger.log(` ${chalk.yellow("●")} Setup not completed`);
39
+ }
40
+
41
+ // Docker services
42
+ logger.log(chalk.bold("\n Docker Services\n"));
43
+ if (isDockerAvailable()) {
44
+ const composePath = findComposeFile([
45
+ process.cwd(),
46
+ "backend/docker",
47
+ ]);
48
+ if (composePath) {
49
+ const status = getServiceStatus(composePath);
50
+ if (status && Array.isArray(status)) {
51
+ for (const svc of status) {
52
+ const running = svc.State === "running";
53
+ const icon = running ? chalk.green("●") : chalk.red("●");
54
+ logger.log(
55
+ ` ${icon} ${svc.Service || svc.Name}: ${svc.State}`,
56
+ );
57
+ }
58
+ } else if (status) {
59
+ logger.log(` ${status}`);
60
+ } else {
61
+ logger.log(` ${chalk.gray("○")} No services running`);
62
+ }
63
+ } else {
64
+ logger.log(` ${chalk.gray("○")} docker-compose.yml not found`);
65
+ }
66
+ } else {
67
+ logger.log(` ${chalk.gray("○")} Docker not available`);
68
+ }
69
+
70
+ // Port checks
71
+ logger.log(chalk.bold("\n Ports\n"));
72
+ const portChecks = Object.entries(DEFAULT_PORTS).map(
73
+ async ([name, port]) => {
74
+ const open = await checkPort(port);
75
+ const icon = open ? chalk.green("●") : chalk.gray("○");
76
+ return ` ${icon} ${name}: ${port}`;
77
+ },
78
+ );
79
+ const results = await Promise.all(portChecks);
80
+ for (const line of results) {
81
+ logger.log(line);
82
+ }
83
+
84
+ logger.newline();
85
+ } catch (err) {
86
+ logger.error(`Status check failed: ${err.message}`);
87
+ process.exit(1);
88
+ }
89
+ });
90
+ }
91
+
92
+ function checkPort(port, host = "127.0.0.1") {
93
+ return new Promise((resolve) => {
94
+ const socket = createConnection({ port, host, timeout: 1000 });
95
+ socket.on("connect", () => {
96
+ socket.destroy();
97
+ resolve(true);
98
+ });
99
+ socket.on("error", () => resolve(false));
100
+ socket.on("timeout", () => {
101
+ socket.destroy();
102
+ resolve(false);
103
+ });
104
+ });
105
+ }
@@ -0,0 +1,49 @@
1
+ import { stopApp, isAppRunning } from "../lib/process-manager.js";
2
+ import {
3
+ servicesDown,
4
+ findComposeFile,
5
+ isDockerAvailable,
6
+ } from "../lib/service-manager.js";
7
+ import logger from "../lib/logger.js";
8
+
9
+ export function registerStopCommand(program) {
10
+ program
11
+ .command("stop")
12
+ .description("Stop ChainlessChain")
13
+ .option("--services", "Stop Docker services only")
14
+ .option("--all", "Stop both app and Docker services")
15
+ .action(async (options) => {
16
+ try {
17
+ if (options.services || options.all) {
18
+ if (isDockerAvailable()) {
19
+ const composePath = findComposeFile([
20
+ process.cwd(),
21
+ "backend/docker",
22
+ ]);
23
+ if (composePath) {
24
+ logger.info("Stopping Docker services...");
25
+ servicesDown(composePath);
26
+ logger.success("Docker services stopped");
27
+ } else {
28
+ logger.warn("docker-compose.yml not found");
29
+ }
30
+ } else {
31
+ logger.warn("Docker not available");
32
+ }
33
+ }
34
+
35
+ if (!options.services) {
36
+ if (isAppRunning()) {
37
+ logger.info("Stopping ChainlessChain...");
38
+ stopApp();
39
+ logger.success("ChainlessChain stopped");
40
+ } else {
41
+ logger.info("ChainlessChain is not running");
42
+ }
43
+ }
44
+ } catch (err) {
45
+ logger.error(`Failed to stop: ${err.message}`);
46
+ process.exit(1);
47
+ }
48
+ });
49
+ }
@@ -0,0 +1,78 @@
1
+ import chalk from "chalk";
2
+ import { checkForUpdates } from "../lib/version-checker.js";
3
+ import { downloadRelease } from "../lib/downloader.js";
4
+ import { VERSION } from "../constants.js";
5
+ import { askConfirm } from "../lib/prompts.js";
6
+ import logger from "../lib/logger.js";
7
+
8
+ export function registerUpdateCommand(program) {
9
+ program
10
+ .command("update")
11
+ .description("Check for and install updates")
12
+ .option("--check", "Only check for updates (do not download)")
13
+ .option(
14
+ "--channel <channel>",
15
+ "Release channel: stable, beta, dev",
16
+ "stable",
17
+ )
18
+ .option("--force", "Re-download even if binary exists")
19
+ .action(async (options) => {
20
+ try {
21
+ logger.info(`Current version: ${VERSION}`);
22
+ logger.info(`Channel: ${options.channel}`);
23
+ logger.newline();
24
+
25
+ const result = await checkForUpdates({ channel: options.channel });
26
+
27
+ if (result.error) {
28
+ logger.warn(`Update check failed: ${result.error}`);
29
+ return;
30
+ }
31
+
32
+ if (!result.updateAvailable) {
33
+ logger.success("You are on the latest version");
34
+ return;
35
+ }
36
+
37
+ logger.log(
38
+ chalk.bold(
39
+ `\n Update available: ${result.currentVersion} → ${chalk.green(result.latestVersion)}\n`,
40
+ ),
41
+ );
42
+
43
+ if (result.publishedAt) {
44
+ logger.log(
45
+ ` Published: ${new Date(result.publishedAt).toLocaleDateString()}`,
46
+ );
47
+ }
48
+ if (result.releaseUrl) {
49
+ logger.log(` Release: ${result.releaseUrl}`);
50
+ }
51
+ logger.newline();
52
+
53
+ if (options.check) {
54
+ return;
55
+ }
56
+
57
+ const doUpdate = await askConfirm(
58
+ `Download v${result.latestVersion}?`,
59
+ true,
60
+ );
61
+ if (!doUpdate) {
62
+ logger.info("Update cancelled");
63
+ return;
64
+ }
65
+
66
+ await downloadRelease(result.latestVersion, { force: options.force });
67
+ logger.success(`Updated to v${result.latestVersion}`);
68
+ logger.info("Restart ChainlessChain to use the new version.");
69
+ } catch (err) {
70
+ if (err.name === "ExitPromptError") {
71
+ logger.log("\nUpdate cancelled.");
72
+ process.exit(0);
73
+ }
74
+ logger.error(`Update failed: ${err.message}`);
75
+ process.exit(1);
76
+ }
77
+ });
78
+ }
@@ -0,0 +1,112 @@
1
+ import { createRequire } from "node:module";
2
+ import { fileURLToPath } from "node:url";
3
+ import { dirname, join } from "node:path";
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = dirname(__filename);
7
+ const require = createRequire(import.meta.url);
8
+ const pkg = require(join(__dirname, "..", "package.json"));
9
+
10
+ export const VERSION = pkg.version;
11
+
12
+ export const GITHUB_OWNER = "yourname";
13
+ export const GITHUB_REPO = "chainlesschain";
14
+ export const GITHUB_API_BASE = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}`;
15
+ export const GITHUB_RELEASES_URL = `${GITHUB_API_BASE}/releases`;
16
+
17
+ export const DEFAULT_PORTS = {
18
+ vite: 5173,
19
+ signaling: 9001,
20
+ ollama: 11434,
21
+ qdrant: 6333,
22
+ postgresql: 5432,
23
+ redis: 6379,
24
+ projectService: 9090,
25
+ aiService: 8001,
26
+ };
27
+
28
+ export const LLM_PROVIDERS = {
29
+ ollama: {
30
+ name: "Ollama (Local)",
31
+ defaultBaseUrl: "http://localhost:11434",
32
+ defaultModel: "qwen2:7b",
33
+ requiresApiKey: false,
34
+ },
35
+ openai: {
36
+ name: "OpenAI",
37
+ defaultBaseUrl: "https://api.openai.com/v1",
38
+ defaultModel: "gpt-4o",
39
+ requiresApiKey: true,
40
+ },
41
+ dashscope: {
42
+ name: "DashScope (Alibaba)",
43
+ defaultBaseUrl: "https://dashscope.aliyuncs.com/api/v1",
44
+ defaultModel: "qwen-max",
45
+ requiresApiKey: true,
46
+ },
47
+ deepseek: {
48
+ name: "DeepSeek",
49
+ defaultBaseUrl: "https://api.deepseek.com/v1",
50
+ defaultModel: "deepseek-chat",
51
+ requiresApiKey: true,
52
+ },
53
+ custom: {
54
+ name: "Custom Provider",
55
+ defaultBaseUrl: "",
56
+ defaultModel: "",
57
+ requiresApiKey: true,
58
+ },
59
+ };
60
+
61
+ export const EDITIONS = {
62
+ personal: {
63
+ name: "Personal",
64
+ description: "For individual use with local AI and privacy-first design",
65
+ },
66
+ enterprise: {
67
+ name: "Enterprise",
68
+ description: "For teams with SSO, RBAC, audit logging, and org management",
69
+ },
70
+ };
71
+
72
+ export const BINARY_NAMES = {
73
+ win32: { x64: "chainlesschain-desktop-vue-{version}-win-x64.exe" },
74
+ darwin: {
75
+ x64: "chainlesschain-desktop-vue-{version}-mac-x64.dmg",
76
+ arm64: "chainlesschain-desktop-vue-{version}-mac-arm64.dmg",
77
+ },
78
+ linux: { x64: "chainlesschain-desktop-vue-{version}-linux-x64.deb" },
79
+ };
80
+
81
+ export const CONFIG_DIR_NAME = ".chainlesschain";
82
+
83
+ export const DEFAULT_CONFIG = {
84
+ setupCompleted: false,
85
+ completedAt: null,
86
+ edition: "personal",
87
+ paths: {
88
+ projectRoot: null,
89
+ database: null,
90
+ },
91
+ llm: {
92
+ provider: "ollama",
93
+ apiKey: null,
94
+ baseUrl: "http://localhost:11434",
95
+ model: "qwen2:7b",
96
+ },
97
+ enterprise: {
98
+ serverUrl: null,
99
+ apiKey: null,
100
+ tenantId: null,
101
+ },
102
+ services: {
103
+ autoStart: false,
104
+ dockerComposePath: null,
105
+ },
106
+ update: {
107
+ channel: "stable",
108
+ autoCheck: true,
109
+ },
110
+ };
111
+
112
+ export const MIN_NODE_VERSION = "22.12.0";
package/src/index.js ADDED
@@ -0,0 +1,34 @@
1
+ import { Command } from "commander";
2
+ import { VERSION } from "./constants.js";
3
+ import { registerSetupCommand } from "./commands/setup.js";
4
+ import { registerStartCommand } from "./commands/start.js";
5
+ import { registerStopCommand } from "./commands/stop.js";
6
+ import { registerStatusCommand } from "./commands/status.js";
7
+ import { registerServicesCommand } from "./commands/services.js";
8
+ import { registerConfigCommand } from "./commands/config.js";
9
+ import { registerUpdateCommand } from "./commands/update.js";
10
+ import { registerDoctorCommand } from "./commands/doctor.js";
11
+
12
+ export function createProgram() {
13
+ const program = new Command();
14
+
15
+ program
16
+ .name("chainlesschain")
17
+ .description(
18
+ "CLI for ChainlessChain - install, configure, and manage your personal AI management system",
19
+ )
20
+ .version(VERSION, "-v, --version")
21
+ .option("--verbose", "Enable verbose output")
22
+ .option("--quiet", "Suppress non-essential output");
23
+
24
+ registerSetupCommand(program);
25
+ registerStartCommand(program);
26
+ registerStopCommand(program);
27
+ registerStatusCommand(program);
28
+ registerServicesCommand(program);
29
+ registerConfigCommand(program);
30
+ registerUpdateCommand(program);
31
+ registerDoctorCommand(program);
32
+
33
+ return program;
34
+ }
@@ -0,0 +1,20 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+
4
+ export function computeSha256(filePath) {
5
+ return new Promise((resolve, reject) => {
6
+ const hash = createHash("sha256");
7
+ const stream = createReadStream(filePath);
8
+ stream.on("data", (chunk) => hash.update(chunk));
9
+ stream.on("end", () => resolve(hash.digest("hex")));
10
+ stream.on("error", reject);
11
+ });
12
+ }
13
+
14
+ export function verifySha256(filePath, expectedHash) {
15
+ return computeSha256(filePath).then((actual) => ({
16
+ valid: actual === expectedHash.toLowerCase(),
17
+ actual,
18
+ expected: expectedHash.toLowerCase(),
19
+ }));
20
+ }