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
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { getConfigPath } from "./paths.js";
|
|
3
|
+
import { DEFAULT_CONFIG } from "../constants.js";
|
|
4
|
+
|
|
5
|
+
export function loadConfig() {
|
|
6
|
+
const configPath = getConfigPath();
|
|
7
|
+
if (!existsSync(configPath)) {
|
|
8
|
+
return { ...structuredClone(DEFAULT_CONFIG) };
|
|
9
|
+
}
|
|
10
|
+
try {
|
|
11
|
+
const raw = readFileSync(configPath, "utf-8");
|
|
12
|
+
const parsed = JSON.parse(raw);
|
|
13
|
+
return deepMerge(structuredClone(DEFAULT_CONFIG), parsed);
|
|
14
|
+
} catch {
|
|
15
|
+
return { ...structuredClone(DEFAULT_CONFIG) };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function saveConfig(config) {
|
|
20
|
+
const configPath = getConfigPath();
|
|
21
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function getConfigValue(key) {
|
|
25
|
+
const config = loadConfig();
|
|
26
|
+
return getNestedValue(config, key);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function setConfigValue(key, value) {
|
|
30
|
+
const config = loadConfig();
|
|
31
|
+
setNestedValue(config, key, parseValue(value));
|
|
32
|
+
saveConfig(config);
|
|
33
|
+
return config;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function resetConfig() {
|
|
37
|
+
const config = structuredClone(DEFAULT_CONFIG);
|
|
38
|
+
saveConfig(config);
|
|
39
|
+
return config;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function listConfig() {
|
|
43
|
+
return loadConfig();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getNestedValue(obj, key) {
|
|
47
|
+
const parts = key.split(".");
|
|
48
|
+
let current = obj;
|
|
49
|
+
for (const part of parts) {
|
|
50
|
+
if (current == null || typeof current !== "object") return undefined;
|
|
51
|
+
current = current[part];
|
|
52
|
+
}
|
|
53
|
+
return current;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function setNestedValue(obj, key, value) {
|
|
57
|
+
const parts = key.split(".");
|
|
58
|
+
let current = obj;
|
|
59
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
60
|
+
if (current[parts[i]] == null || typeof current[parts[i]] !== "object") {
|
|
61
|
+
current[parts[i]] = {};
|
|
62
|
+
}
|
|
63
|
+
current = current[parts[i]];
|
|
64
|
+
}
|
|
65
|
+
current[parts[parts.length - 1]] = value;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function parseValue(value) {
|
|
69
|
+
if (value === "true") return true;
|
|
70
|
+
if (value === "false") return false;
|
|
71
|
+
if (value === "null") return null;
|
|
72
|
+
const num = Number(value);
|
|
73
|
+
if (!isNaN(num) && value.trim() !== "") return num;
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function deepMerge(target, source) {
|
|
78
|
+
const result = { ...target };
|
|
79
|
+
for (const key of Object.keys(source)) {
|
|
80
|
+
if (
|
|
81
|
+
source[key] &&
|
|
82
|
+
typeof source[key] === "object" &&
|
|
83
|
+
!Array.isArray(source[key]) &&
|
|
84
|
+
target[key] &&
|
|
85
|
+
typeof target[key] === "object" &&
|
|
86
|
+
!Array.isArray(target[key])
|
|
87
|
+
) {
|
|
88
|
+
result[key] = deepMerge(target[key], source[key]);
|
|
89
|
+
} else {
|
|
90
|
+
result[key] = source[key];
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { createWriteStream, existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { pipeline } from "node:stream/promises";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import ora from "ora";
|
|
6
|
+
import { GITHUB_RELEASES_URL } from "../constants.js";
|
|
7
|
+
import { getBinDir, ensureDir } from "./paths.js";
|
|
8
|
+
import { getBinaryName } from "./platform.js";
|
|
9
|
+
import { verifySha256 } from "./checksum.js";
|
|
10
|
+
import logger from "./logger.js";
|
|
11
|
+
|
|
12
|
+
export async function downloadRelease(version, options = {}) {
|
|
13
|
+
const binaryName = getBinaryName(version);
|
|
14
|
+
const binDir = ensureDir(getBinDir());
|
|
15
|
+
const destPath = join(binDir, binaryName);
|
|
16
|
+
|
|
17
|
+
if (existsSync(destPath) && !options.force) {
|
|
18
|
+
logger.info(`Binary already exists: ${binaryName}`);
|
|
19
|
+
return destPath;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const releaseUrl =
|
|
23
|
+
options.url || (await resolveAssetUrl(version, binaryName));
|
|
24
|
+
const spinner = ora(`Downloading ${binaryName}...`).start();
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const response = await fetch(releaseUrl, {
|
|
28
|
+
headers: { Accept: "application/octet-stream" },
|
|
29
|
+
redirect: "follow",
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`Download failed: HTTP ${response.status} ${response.statusText}`,
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const totalBytes = parseInt(
|
|
39
|
+
response.headers.get("content-length") || "0",
|
|
40
|
+
10,
|
|
41
|
+
);
|
|
42
|
+
let downloadedBytes = 0;
|
|
43
|
+
|
|
44
|
+
const reader = response.body.getReader();
|
|
45
|
+
const stream = new ReadableStream({
|
|
46
|
+
async pull(controller) {
|
|
47
|
+
const { done, value } = await reader.read();
|
|
48
|
+
if (done) {
|
|
49
|
+
controller.close();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
downloadedBytes += value.byteLength;
|
|
53
|
+
if (totalBytes > 0) {
|
|
54
|
+
const pct = ((downloadedBytes / totalBytes) * 100).toFixed(1);
|
|
55
|
+
spinner.text = `Downloading ${binaryName}... ${pct}% (${formatBytes(downloadedBytes)}/${formatBytes(totalBytes)})`;
|
|
56
|
+
}
|
|
57
|
+
controller.enqueue(value);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const nodeStream = Readable.fromWeb(stream);
|
|
62
|
+
await pipeline(nodeStream, createWriteStream(destPath));
|
|
63
|
+
|
|
64
|
+
spinner.succeed(
|
|
65
|
+
`Downloaded ${binaryName} (${formatBytes(downloadedBytes)})`,
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (options.checksum) {
|
|
69
|
+
const verifySpinner = ora("Verifying checksum...").start();
|
|
70
|
+
const result = await verifySha256(destPath, options.checksum);
|
|
71
|
+
if (!result.valid) {
|
|
72
|
+
unlinkSync(destPath);
|
|
73
|
+
verifySpinner.fail("Checksum verification failed");
|
|
74
|
+
throw new Error(
|
|
75
|
+
`SHA256 mismatch: expected ${result.expected}, got ${result.actual}`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
verifySpinner.succeed("Checksum verified");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return destPath;
|
|
82
|
+
} catch (err) {
|
|
83
|
+
spinner.fail(`Download failed: ${err.message}`);
|
|
84
|
+
if (existsSync(destPath)) {
|
|
85
|
+
unlinkSync(destPath);
|
|
86
|
+
}
|
|
87
|
+
throw err;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function resolveAssetUrl(version, binaryName) {
|
|
92
|
+
const tagName = `v${version}`;
|
|
93
|
+
const url = `${GITHUB_RELEASES_URL}/tags/${tagName}`;
|
|
94
|
+
|
|
95
|
+
const response = await fetch(url, {
|
|
96
|
+
headers: { Accept: "application/vnd.github.v3+json" },
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
throw new Error(
|
|
101
|
+
`Failed to find release ${tagName}: HTTP ${response.status}`,
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const release = await response.json();
|
|
106
|
+
const asset = release.assets.find((a) => a.name === binaryName);
|
|
107
|
+
if (!asset) {
|
|
108
|
+
throw new Error(`Asset ${binaryName} not found in release ${tagName}`);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return asset.browser_download_url;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function formatBytes(bytes) {
|
|
115
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
116
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
117
|
+
if (bytes < 1024 * 1024 * 1024)
|
|
118
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
|
|
119
|
+
return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}GB`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { resolveAssetUrl, formatBytes };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
|
|
3
|
+
let verboseEnabled = false;
|
|
4
|
+
let quietEnabled = false;
|
|
5
|
+
|
|
6
|
+
export function setVerbose(enabled) {
|
|
7
|
+
verboseEnabled = enabled;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function setQuiet(enabled) {
|
|
11
|
+
quietEnabled = enabled;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function info(message, ...args) {
|
|
15
|
+
if (!quietEnabled) {
|
|
16
|
+
console.log(chalk.blue("ℹ"), message, ...args);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function success(message, ...args) {
|
|
21
|
+
if (!quietEnabled) {
|
|
22
|
+
console.log(chalk.green("✔"), message, ...args);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function warn(message, ...args) {
|
|
27
|
+
console.log(chalk.yellow("⚠"), message, ...args);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function error(message, ...args) {
|
|
31
|
+
console.error(chalk.red("✖"), message, ...args);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function verbose(message, ...args) {
|
|
35
|
+
if (verboseEnabled) {
|
|
36
|
+
console.log(chalk.gray("⋯"), message, ...args);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function log(message, ...args) {
|
|
41
|
+
if (!quietEnabled) {
|
|
42
|
+
console.log(message, ...args);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function newline() {
|
|
47
|
+
if (!quietEnabled) {
|
|
48
|
+
console.log();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const logger = {
|
|
53
|
+
info,
|
|
54
|
+
success,
|
|
55
|
+
warn,
|
|
56
|
+
error,
|
|
57
|
+
verbose,
|
|
58
|
+
log,
|
|
59
|
+
newline,
|
|
60
|
+
setVerbose,
|
|
61
|
+
setQuiet,
|
|
62
|
+
};
|
|
63
|
+
export default logger;
|
package/src/lib/paths.js
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { mkdirSync, existsSync } from "node:fs";
|
|
4
|
+
import { CONFIG_DIR_NAME } from "../constants.js";
|
|
5
|
+
import { getPlatform } from "./platform.js";
|
|
6
|
+
|
|
7
|
+
export function getHomeDir() {
|
|
8
|
+
return join(homedir(), CONFIG_DIR_NAME);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function getBinDir() {
|
|
12
|
+
return join(getHomeDir(), "bin");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getConfigPath() {
|
|
16
|
+
return join(getHomeDir(), "config.json");
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getStatePath() {
|
|
20
|
+
return join(getHomeDir(), "state");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getPidFilePath() {
|
|
24
|
+
return join(getStatePath(), "app.pid");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getServicesDir() {
|
|
28
|
+
return join(getHomeDir(), "services");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getLogsDir() {
|
|
32
|
+
return join(getHomeDir(), "logs");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getCacheDir() {
|
|
36
|
+
return join(getHomeDir(), "cache");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getElectronUserDataDir() {
|
|
40
|
+
const p = getPlatform();
|
|
41
|
+
const appName = "chainlesschain-desktop-vue";
|
|
42
|
+
switch (p) {
|
|
43
|
+
case "win32":
|
|
44
|
+
return join(
|
|
45
|
+
process.env.APPDATA || join(homedir(), "AppData", "Roaming"),
|
|
46
|
+
appName,
|
|
47
|
+
);
|
|
48
|
+
case "darwin":
|
|
49
|
+
return join(homedir(), "Library", "Application Support", appName);
|
|
50
|
+
case "linux":
|
|
51
|
+
return join(
|
|
52
|
+
process.env.XDG_CONFIG_HOME || join(homedir(), ".config"),
|
|
53
|
+
appName,
|
|
54
|
+
);
|
|
55
|
+
default:
|
|
56
|
+
return join(homedir(), appName);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function ensureDir(dirPath) {
|
|
61
|
+
if (!existsSync(dirPath)) {
|
|
62
|
+
mkdirSync(dirPath, { recursive: true });
|
|
63
|
+
}
|
|
64
|
+
return dirPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function ensureHomeDir() {
|
|
68
|
+
const dirs = [
|
|
69
|
+
getHomeDir(),
|
|
70
|
+
getBinDir(),
|
|
71
|
+
getStatePath(),
|
|
72
|
+
getServicesDir(),
|
|
73
|
+
getLogsDir(),
|
|
74
|
+
getCacheDir(),
|
|
75
|
+
];
|
|
76
|
+
for (const dir of dirs) {
|
|
77
|
+
ensureDir(dir);
|
|
78
|
+
}
|
|
79
|
+
return getHomeDir();
|
|
80
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { platform, arch } from "node:os";
|
|
2
|
+
import { BINARY_NAMES } from "../constants.js";
|
|
3
|
+
|
|
4
|
+
export function getPlatform() {
|
|
5
|
+
return platform();
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function getArch() {
|
|
9
|
+
const a = arch();
|
|
10
|
+
if (a === "arm64") return "arm64";
|
|
11
|
+
if (a === "x64" || a === "x86_64") return "x64";
|
|
12
|
+
return a;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function getBinaryName(version) {
|
|
16
|
+
const p = getPlatform();
|
|
17
|
+
const a = getArch();
|
|
18
|
+
const platformBinaries = BINARY_NAMES[p];
|
|
19
|
+
if (!platformBinaries) {
|
|
20
|
+
throw new Error(`Unsupported platform: ${p}`);
|
|
21
|
+
}
|
|
22
|
+
const template = platformBinaries[a];
|
|
23
|
+
if (!template) {
|
|
24
|
+
throw new Error(`Unsupported architecture: ${a} on ${p}`);
|
|
25
|
+
}
|
|
26
|
+
return template.replace("{version}", version);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getBinaryExtension() {
|
|
30
|
+
const p = getPlatform();
|
|
31
|
+
switch (p) {
|
|
32
|
+
case "win32":
|
|
33
|
+
return ".exe";
|
|
34
|
+
case "darwin":
|
|
35
|
+
return ".dmg";
|
|
36
|
+
case "linux":
|
|
37
|
+
return ".deb";
|
|
38
|
+
default:
|
|
39
|
+
return "";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function isWindows() {
|
|
44
|
+
return getPlatform() === "win32";
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isMac() {
|
|
48
|
+
return getPlatform() === "darwin";
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function isLinux() {
|
|
52
|
+
return getPlatform() === "linux";
|
|
53
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { spawn, execSync } from "node:child_process";
|
|
2
|
+
import {
|
|
3
|
+
readFileSync,
|
|
4
|
+
writeFileSync,
|
|
5
|
+
unlinkSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
readdirSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { getPidFilePath, getBinDir, ensureDir, getStatePath } from "./paths.js";
|
|
11
|
+
import { isWindows } from "./platform.js";
|
|
12
|
+
import logger from "./logger.js";
|
|
13
|
+
|
|
14
|
+
export function startApp(options = {}) {
|
|
15
|
+
ensureDir(getStatePath());
|
|
16
|
+
const pidFile = getPidFilePath();
|
|
17
|
+
|
|
18
|
+
if (isAppRunning()) {
|
|
19
|
+
logger.warn("ChainlessChain is already running");
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const binDir = getBinDir();
|
|
24
|
+
let appPath;
|
|
25
|
+
|
|
26
|
+
if (options.appPath) {
|
|
27
|
+
appPath = options.appPath;
|
|
28
|
+
} else if (isWindows()) {
|
|
29
|
+
appPath = findExecutable(binDir, ".exe");
|
|
30
|
+
} else {
|
|
31
|
+
appPath = findExecutable(binDir);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!appPath) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'ChainlessChain binary not found. Run "chainlesschain setup" first.',
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const args = [];
|
|
41
|
+
if (options.headless) {
|
|
42
|
+
args.push("--headless");
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
logger.verbose(`Starting: ${appPath} ${args.join(" ")}`);
|
|
46
|
+
|
|
47
|
+
const child = spawn(appPath, args, {
|
|
48
|
+
detached: true,
|
|
49
|
+
stdio: "ignore",
|
|
50
|
+
env: { ...process.env, ...options.env },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
child.unref();
|
|
54
|
+
|
|
55
|
+
writeFileSync(pidFile, String(child.pid), "utf-8");
|
|
56
|
+
logger.verbose(`PID ${child.pid} written to ${pidFile}`);
|
|
57
|
+
|
|
58
|
+
return child.pid;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function stopApp() {
|
|
62
|
+
const pidFile = getPidFilePath();
|
|
63
|
+
|
|
64
|
+
if (!existsSync(pidFile)) {
|
|
65
|
+
logger.warn("No PID file found. App may not be running.");
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
if (isWindows()) {
|
|
73
|
+
execSync(`taskkill /PID ${pid} /T /F`, { stdio: "ignore" });
|
|
74
|
+
} else {
|
|
75
|
+
process.kill(pid, "SIGTERM");
|
|
76
|
+
}
|
|
77
|
+
logger.verbose(`Sent stop signal to PID ${pid}`);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
logger.verbose(`Process ${pid} may have already exited: ${err.message}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
unlinkSync(pidFile);
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function isAppRunning() {
|
|
87
|
+
const pidFile = getPidFilePath();
|
|
88
|
+
|
|
89
|
+
if (!existsSync(pidFile)) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const pid = parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
process.kill(pid, 0);
|
|
97
|
+
return true;
|
|
98
|
+
} catch {
|
|
99
|
+
unlinkSync(pidFile);
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function getAppPid() {
|
|
105
|
+
const pidFile = getPidFilePath();
|
|
106
|
+
if (!existsSync(pidFile)) return null;
|
|
107
|
+
return parseInt(readFileSync(pidFile, "utf-8").trim(), 10);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function findExecutable(binDir, extension) {
|
|
111
|
+
if (!existsSync(binDir)) return null;
|
|
112
|
+
try {
|
|
113
|
+
const files = readdirSync(binDir);
|
|
114
|
+
const match = files.find((f) => {
|
|
115
|
+
const isChainless = f.toLowerCase().includes("chainlesschain");
|
|
116
|
+
if (extension) return isChainless && f.endsWith(extension);
|
|
117
|
+
return (
|
|
118
|
+
isChainless &&
|
|
119
|
+
!f.endsWith(".dmg") &&
|
|
120
|
+
!f.endsWith(".deb") &&
|
|
121
|
+
!f.endsWith(".sha256")
|
|
122
|
+
);
|
|
123
|
+
});
|
|
124
|
+
return match ? join(binDir, match) : null;
|
|
125
|
+
} catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { confirm, select, input, password } from "@inquirer/prompts";
|
|
2
|
+
|
|
3
|
+
export async function askConfirm(message, defaultValue = true) {
|
|
4
|
+
return confirm({ message, default: defaultValue });
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export async function askSelect(message, choices) {
|
|
8
|
+
return select({ message, choices });
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function askInput(message, defaultValue = "") {
|
|
12
|
+
return input({ message, default: defaultValue });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function askPassword(message) {
|
|
16
|
+
return password({ message, mask: "*" });
|
|
17
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { execSync, spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import logger from "./logger.js";
|
|
5
|
+
|
|
6
|
+
export function isDockerAvailable() {
|
|
7
|
+
try {
|
|
8
|
+
execSync("docker --version", { stdio: "ignore" });
|
|
9
|
+
return true;
|
|
10
|
+
} catch {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function isDockerComposeAvailable() {
|
|
16
|
+
try {
|
|
17
|
+
execSync("docker compose version", { stdio: "ignore" });
|
|
18
|
+
return true;
|
|
19
|
+
} catch {
|
|
20
|
+
try {
|
|
21
|
+
execSync("docker-compose --version", { stdio: "ignore" });
|
|
22
|
+
return true;
|
|
23
|
+
} catch {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function getComposeCommand() {
|
|
30
|
+
try {
|
|
31
|
+
execSync("docker compose version", { stdio: "ignore" });
|
|
32
|
+
return "docker compose";
|
|
33
|
+
} catch {
|
|
34
|
+
return "docker-compose";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function findComposeFile(searchPaths) {
|
|
39
|
+
const names = [
|
|
40
|
+
"docker-compose.yml",
|
|
41
|
+
"docker-compose.yaml",
|
|
42
|
+
"compose.yml",
|
|
43
|
+
"compose.yaml",
|
|
44
|
+
];
|
|
45
|
+
for (const dir of searchPaths) {
|
|
46
|
+
for (const name of names) {
|
|
47
|
+
const filePath = join(dir, name);
|
|
48
|
+
if (existsSync(filePath)) {
|
|
49
|
+
return filePath;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function servicesUp(composePath, options = {}) {
|
|
57
|
+
const cmd = getComposeCommand();
|
|
58
|
+
const args = ["-f", composePath, "up", "-d"];
|
|
59
|
+
if (options.services) {
|
|
60
|
+
args.push(...options.services);
|
|
61
|
+
}
|
|
62
|
+
return runCompose(cmd, args);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function servicesDown(composePath) {
|
|
66
|
+
const cmd = getComposeCommand();
|
|
67
|
+
return runCompose(cmd, ["-f", composePath, "down"]);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function servicesLogs(composePath, options = {}) {
|
|
71
|
+
const cmd = getComposeCommand();
|
|
72
|
+
const args = ["-f", composePath, "logs"];
|
|
73
|
+
if (options.follow) args.push("-f");
|
|
74
|
+
if (options.tail) args.push("--tail", String(options.tail));
|
|
75
|
+
if (options.services) args.push(...options.services);
|
|
76
|
+
|
|
77
|
+
const parts = cmd.split(" ");
|
|
78
|
+
const child = spawn(parts[0], [...parts.slice(1), ...args], {
|
|
79
|
+
stdio: "inherit",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return new Promise((resolve, reject) => {
|
|
83
|
+
child.on("close", (code) => {
|
|
84
|
+
if (code === 0) resolve();
|
|
85
|
+
else reject(new Error(`docker compose logs exited with code ${code}`));
|
|
86
|
+
});
|
|
87
|
+
child.on("error", reject);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function servicesPull(composePath) {
|
|
92
|
+
const cmd = getComposeCommand();
|
|
93
|
+
return runCompose(cmd, ["-f", composePath, "pull"]);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getServiceStatus(composePath) {
|
|
97
|
+
const cmd = getComposeCommand();
|
|
98
|
+
try {
|
|
99
|
+
const output = execSync(`${cmd} -f "${composePath}" ps --format json`, {
|
|
100
|
+
encoding: "utf-8",
|
|
101
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
102
|
+
});
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(`[${output.trim().split("\n").join(",")}]`);
|
|
105
|
+
} catch {
|
|
106
|
+
return output.trim();
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function runCompose(cmd, args) {
|
|
114
|
+
const fullCmd = `${cmd} ${args.map((a) => (a.includes(" ") ? `"${a}"` : a)).join(" ")}`;
|
|
115
|
+
logger.verbose(`Running: ${fullCmd}`);
|
|
116
|
+
try {
|
|
117
|
+
execSync(fullCmd, { stdio: "inherit" });
|
|
118
|
+
return true;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
logger.error(`Command failed: ${fullCmd}`);
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
}
|