tingly-box-bundle 0.0.37-dev
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/bin.js +22 -0
- package/lib/cache.js +49 -0
- package/lib/cli.js +175 -0
- package/lib/config.js +122 -0
- package/lib/downloader.js +156 -0
- package/lib/executor.js +122 -0
- package/lib/flavors/bundle.json +6 -0
- package/lib/flavors/cli.json +6 -0
- package/lib/flavors/gui.json +6 -0
- package/lib/platform.js +75 -0
- package/package.json +53 -0
package/bin.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Tingly-Box Bundle Package
|
|
5
|
+
*
|
|
6
|
+
* This package has platform-specific packages as optionalDependencies.
|
|
7
|
+
* npm will automatically install the correct platform package when you install this.
|
|
8
|
+
*
|
|
9
|
+
* The platform package (@tingly-box/darwin-arm64, etc.) contains the pre-built binary.
|
|
10
|
+
* If the platform package is installed, we use it directly.
|
|
11
|
+
* Otherwise, we fall back to downloading from GitHub (same as CLI).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { run } from "@tingly-box/shared/cli.js";
|
|
15
|
+
|
|
16
|
+
// Set release tag from package.json or environment
|
|
17
|
+
const releaseTag = process.env.TINGLY_RELEASE_TAG || "latest";
|
|
18
|
+
|
|
19
|
+
run("bundle", { releaseTag }).catch((error) => {
|
|
20
|
+
console.error(`❌ Fatal error: ${error.message}`);
|
|
21
|
+
process.exit(1);
|
|
22
|
+
});
|
package/lib/cache.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache directory management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { join } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Get cache directory
|
|
10
|
+
* @param {string} flavor - Flavor name (cli, gui, bundle)
|
|
11
|
+
* @param {string} version - Version string
|
|
12
|
+
* @returns {string} Cache directory path
|
|
13
|
+
*/
|
|
14
|
+
export function getCacheDir(flavor, version) {
|
|
15
|
+
let baseDir;
|
|
16
|
+
|
|
17
|
+
if (process.platform === "linux") {
|
|
18
|
+
baseDir = process.env.XDG_CACHE_HOME || join(homedir(), ".cache");
|
|
19
|
+
} else if (process.platform === "darwin") {
|
|
20
|
+
baseDir = join(homedir(), "Library", "Caches");
|
|
21
|
+
} else if (process.platform === "win32") {
|
|
22
|
+
baseDir = process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return join(baseDir, "tingly-box", flavor, version);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Get binary cache path
|
|
30
|
+
* @param {string} flavor - Flavor name
|
|
31
|
+
* @param {string} version - Version string
|
|
32
|
+
* @param {string} binaryName - Binary name
|
|
33
|
+
* @returns {string} Binary cache path
|
|
34
|
+
*/
|
|
35
|
+
export function getBinaryCachePath(flavor, version, binaryName) {
|
|
36
|
+
const cacheDir = getCacheDir(flavor, version);
|
|
37
|
+
const suffix = process.platform === "win32" ? ".exe" : "";
|
|
38
|
+
return join(cacheDir, binaryName + suffix);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get app bundle cache path (macOS GUI)
|
|
43
|
+
* @param {string} version - Version string
|
|
44
|
+
* @returns {string} App bundle path
|
|
45
|
+
*/
|
|
46
|
+
export function getAppCachePath(version) {
|
|
47
|
+
const cacheDir = getCacheDir("gui", version);
|
|
48
|
+
return join(cacheDir, "TinglyBox.app");
|
|
49
|
+
}
|
package/lib/cli.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main CLI runner - shared logic for all flavor packages
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { existsSync, mkdirSync } from "fs";
|
|
6
|
+
import { join, dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { createReadStream } from "fs";
|
|
9
|
+
import { mkdir } from "fs/promises";
|
|
10
|
+
import { pipeline } from "stream/promises";
|
|
11
|
+
import unzipper from "unzipper";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
getPlatformInfo,
|
|
15
|
+
getPlatformIdentifier,
|
|
16
|
+
getPlatformPackageName,
|
|
17
|
+
getBinaryName,
|
|
18
|
+
isPlatformSupported,
|
|
19
|
+
} from "./platform.js";
|
|
20
|
+
import {
|
|
21
|
+
getCacheDir,
|
|
22
|
+
getBinaryCachePath,
|
|
23
|
+
getAppCachePath,
|
|
24
|
+
} from "./cache.js";
|
|
25
|
+
import {
|
|
26
|
+
downloadAndExtractZip,
|
|
27
|
+
findBinary,
|
|
28
|
+
ensureExecutable,
|
|
29
|
+
} from "./downloader.js";
|
|
30
|
+
import {
|
|
31
|
+
executeBinary,
|
|
32
|
+
launchApp,
|
|
33
|
+
ensureAppExecutable,
|
|
34
|
+
} from "./executor.js";
|
|
35
|
+
import {
|
|
36
|
+
getFlavorConfig,
|
|
37
|
+
getReleaseBranch,
|
|
38
|
+
getBaseUrl,
|
|
39
|
+
parseTransportVersion,
|
|
40
|
+
} from "./config.js";
|
|
41
|
+
|
|
42
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
43
|
+
const __dirname = dirname(__filename);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Check if platform-specific package is installed
|
|
47
|
+
* @returns {boolean}
|
|
48
|
+
*/
|
|
49
|
+
async function isPlatformPackageInstalled() {
|
|
50
|
+
try {
|
|
51
|
+
const pkgName = getPlatformPackageName();
|
|
52
|
+
const pkgPath = join(process.cwd(), "node_modules", pkgName);
|
|
53
|
+
return existsSync(pkgPath);
|
|
54
|
+
} catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Get binary from platform-specific package
|
|
61
|
+
* @returns {string|null} Binary path or null
|
|
62
|
+
*/
|
|
63
|
+
async function getPlatformPackageBinary() {
|
|
64
|
+
try {
|
|
65
|
+
const pkgName = getPlatformPackageName();
|
|
66
|
+
const pkgPath = join(process.cwd(), "node_modules", pkgName);
|
|
67
|
+
const binaryName = getBinaryName();
|
|
68
|
+
const binaryPath = join(pkgPath, binaryName);
|
|
69
|
+
|
|
70
|
+
if (existsSync(binaryPath)) {
|
|
71
|
+
return binaryPath;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Run CLI for a flavor
|
|
81
|
+
* @param {string} flavor - Flavor name (cli, gui, bundle)
|
|
82
|
+
* @param {Object} options - Options
|
|
83
|
+
* @param {string} [options.releaseTag] - Override release tag
|
|
84
|
+
* @param {string} [options.zipDir] - Local zip directory (for bundle)
|
|
85
|
+
*/
|
|
86
|
+
export async function run(flavor, options = {}) {
|
|
87
|
+
const config = getFlavorConfig(flavor);
|
|
88
|
+
const platformInfo = getPlatformInfo();
|
|
89
|
+
const { version, remainingArgs } = parseTransportVersion(process.argv.slice(2));
|
|
90
|
+
|
|
91
|
+
// Use provided release tag or default
|
|
92
|
+
const releaseTag = options.releaseTag || getReleaseBranch();
|
|
93
|
+
|
|
94
|
+
// Validate platform support
|
|
95
|
+
if (!isPlatformSupported(config.platforms)) {
|
|
96
|
+
console.error(`\n❌ This package does not support ${platformInfo.platform}`);
|
|
97
|
+
console.error(`┌─ Status:`);
|
|
98
|
+
console.error(`│ Supported platforms: ${config.platforms.join(", ")}`);
|
|
99
|
+
console.error(`│ Your platform: ${platformInfo.platform} (${platformInfo.arch})`);
|
|
100
|
+
console.error(`└─ Try: npx tingly-box ${remainingArgs.join(" ")}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Try platform-specific package first
|
|
105
|
+
const platformBinary = await getPlatformPackageBinary();
|
|
106
|
+
if (platformBinary) {
|
|
107
|
+
console.log(`✅ Using platform package: ${getPlatformPackageName()}`);
|
|
108
|
+
executeBinary(platformBinary, remainingArgs, config.defaultArgs);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Otherwise, use remote download or local bundle
|
|
113
|
+
const cacheDir = getCacheDir(flavor, releaseTag);
|
|
114
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
115
|
+
|
|
116
|
+
if (flavor === "gui") {
|
|
117
|
+
await runGui(releaseTag, remainingArgs);
|
|
118
|
+
} else if (flavor === "bundle" || options.zipDir) {
|
|
119
|
+
await runBundle(releaseTag, remainingArgs, options.zipDir);
|
|
120
|
+
} else {
|
|
121
|
+
await runCli(releaseTag, remainingArgs, config);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Run CLI flavor
|
|
127
|
+
*/
|
|
128
|
+
async function runCli(version, args, config) {
|
|
129
|
+
const platformInfo = getPlatformInfo();
|
|
130
|
+
const cacheDir = getCacheDir("cli", version);
|
|
131
|
+
const binaryName = getBinaryName(config.binaryName);
|
|
132
|
+
const binaryPath = getBinaryCachePath("cli", version, config.binaryName);
|
|
133
|
+
|
|
134
|
+
if (!existsSync(binaryPath)) {
|
|
135
|
+
const zipFileName = `${config.binaryName}-${platformInfo.platformDir}-${platformInfo.archDir}.zip`;
|
|
136
|
+
const downloadUrl = `${getBaseUrl()}${version}/${zipFileName}`;
|
|
137
|
+
await downloadAndExtractZip(downloadUrl, cacheDir, "Downloading binary");
|
|
138
|
+
ensureExecutable(binaryPath);
|
|
139
|
+
console.log(`✅ Downloaded to ${binaryPath}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
executeBinary(binaryPath, args, config.defaultArgs);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run GUI flavor
|
|
147
|
+
*/
|
|
148
|
+
async function runGui(version, args) {
|
|
149
|
+
const cacheDir = getCacheDir("gui", version);
|
|
150
|
+
const appPath = getAppCachePath(version);
|
|
151
|
+
|
|
152
|
+
if (!existsSync(appPath)) {
|
|
153
|
+
const platformInfo = getPlatformInfo();
|
|
154
|
+
const zipFileName = `tingly-box-gui-${platformInfo.platformDir}-${platformInfo.archDir}.zip`;
|
|
155
|
+
const downloadUrl = `${getBaseUrl()}${version}/${zipFileName}`;
|
|
156
|
+
await downloadAndExtractZip(downloadUrl, cacheDir, "Downloading GUI");
|
|
157
|
+
ensureAppExecutable(appPath);
|
|
158
|
+
console.log(`✅ Downloaded to ${appPath}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
launchApp(appPath);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Run Bundle flavor
|
|
166
|
+
* Bundle is just a router - it tries to use platform package first,
|
|
167
|
+
* then falls back to GitHub download like CLI
|
|
168
|
+
*/
|
|
169
|
+
async function runBundle(version, args, localZipDir) {
|
|
170
|
+
// Bundle = CLI with better defaults for platform packages
|
|
171
|
+
// The platform package is installed as optionalDependency
|
|
172
|
+
await runCli(version, args, getFlavorConfig("bundle"));
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default { run };
|
package/lib/config.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration management
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readFileSync } from "fs";
|
|
6
|
+
import { join, dirname } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
|
|
9
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
|
|
11
|
+
// Load flavor configurations
|
|
12
|
+
let flavorsConfig = null;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const flavorsPath = join(__dirname, "..", "flavors");
|
|
16
|
+
const cliConfig = JSON.parse(readFileSync(join(flavorsPath, "cli.json"), "utf8"));
|
|
17
|
+
const guiConfig = JSON.parse(readFileSync(join(flavorsPath, "gui.json"), "utf8"));
|
|
18
|
+
const bundleConfig = JSON.parse(readFileSync(join(flavorsPath, "bundle.json"), "utf8"));
|
|
19
|
+
|
|
20
|
+
flavorsConfig = {
|
|
21
|
+
cli: cliConfig,
|
|
22
|
+
gui: guiConfig,
|
|
23
|
+
bundle: bundleConfig,
|
|
24
|
+
};
|
|
25
|
+
} catch (error) {
|
|
26
|
+
// Fallback configurations
|
|
27
|
+
flavorsConfig = {
|
|
28
|
+
cli: {
|
|
29
|
+
binaryName: "tingly-box",
|
|
30
|
+
platforms: ["darwin", "linux", "win32"],
|
|
31
|
+
defaultArgs: ["restart", "--daemon"],
|
|
32
|
+
action: "execute",
|
|
33
|
+
},
|
|
34
|
+
gui: {
|
|
35
|
+
binaryName: "tingly-box-gui",
|
|
36
|
+
platforms: ["darwin"],
|
|
37
|
+
defaultArgs: [],
|
|
38
|
+
action: "launch",
|
|
39
|
+
},
|
|
40
|
+
bundle: {
|
|
41
|
+
binaryName: "tingly-box",
|
|
42
|
+
platforms: ["darwin", "linux", "win32"],
|
|
43
|
+
defaultArgs: ["restart", "--daemon"],
|
|
44
|
+
action: "execute",
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get flavor configuration
|
|
51
|
+
* @param {string} flavor - Flavor name (cli, gui, bundle)
|
|
52
|
+
* @returns {Object} Flavor configuration
|
|
53
|
+
*/
|
|
54
|
+
export function getFlavorConfig(flavor) {
|
|
55
|
+
return flavorsConfig[flavor];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get release branch/tag
|
|
60
|
+
* @returns {string} Release branch (e.g., "latest" or "v1.6.1")
|
|
61
|
+
*/
|
|
62
|
+
export function getReleaseBranch() {
|
|
63
|
+
// This can be replaced during build
|
|
64
|
+
return process.env.TINGLY_RELEASE_BRANCH || "latest";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get base download URL
|
|
69
|
+
* @returns {string} Base URL for downloads
|
|
70
|
+
*/
|
|
71
|
+
export function getBaseUrl() {
|
|
72
|
+
return "https://github.com/tingly-dev/tingly-box/releases/download/";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Parse transport version from arguments
|
|
77
|
+
* @param {string[]} args - Command line arguments
|
|
78
|
+
* @returns {Object} { version, remainingArgs }
|
|
79
|
+
*/
|
|
80
|
+
export function parseTransportVersion(args) {
|
|
81
|
+
let transportVersion = "latest";
|
|
82
|
+
const remainingArgs = [...args];
|
|
83
|
+
|
|
84
|
+
const versionArgIndex = args.findIndex((arg) => arg.startsWith("--transport-version"));
|
|
85
|
+
|
|
86
|
+
if (versionArgIndex !== -1) {
|
|
87
|
+
const versionArg = args[versionArgIndex];
|
|
88
|
+
|
|
89
|
+
if (versionArg.includes("=")) {
|
|
90
|
+
transportVersion = versionArg.split("=")[1];
|
|
91
|
+
remainingArgs.splice(versionArgIndex, 1);
|
|
92
|
+
} else if (versionArgIndex + 1 < args.length) {
|
|
93
|
+
transportVersion = args[versionArgIndex + 1];
|
|
94
|
+
remainingArgs.splice(versionArgIndex, 2);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
version: validateVersion(transportVersion),
|
|
100
|
+
remainingArgs,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate version format
|
|
106
|
+
* @param {string} version - Version string
|
|
107
|
+
* @returns {string} Validated version
|
|
108
|
+
*/
|
|
109
|
+
export function validateVersion(version) {
|
|
110
|
+
if (version === "latest") {
|
|
111
|
+
return version;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const versionRegex = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
115
|
+
if (versionRegex.test(version)) {
|
|
116
|
+
return version;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
console.error(`Invalid version format: ${version}`);
|
|
120
|
+
console.error(`Version must be either "latest" or "v{x.x.x}"`);
|
|
121
|
+
throw new Error(`Invalid version: ${version}`);
|
|
122
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Download and extraction utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { chmodSync, mkdirSync, createWriteStream } from "fs";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { Readable } from "stream";
|
|
8
|
+
import { ProxyAgent } from "undici";
|
|
9
|
+
import unzipper from "unzipper";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get proxy dispatcher
|
|
13
|
+
* @returns {ProxyAgent|undefined}
|
|
14
|
+
*/
|
|
15
|
+
export function getDispatcher() {
|
|
16
|
+
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy;
|
|
17
|
+
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
|
18
|
+
const proxyUri = httpsProxy || httpProxy;
|
|
19
|
+
return proxyUri ? new ProxyAgent(proxyUri) : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Download and extract a ZIP file
|
|
24
|
+
* @param {string} url - Download URL
|
|
25
|
+
* @param {string} extractDir - Destination directory
|
|
26
|
+
* @param {string} [progressPrefix] - Prefix for progress messages
|
|
27
|
+
*/
|
|
28
|
+
export async function downloadAndExtractZip(url, extractDir, progressPrefix = "Downloading") {
|
|
29
|
+
const dispatcher = getDispatcher();
|
|
30
|
+
const fetchOptions = {
|
|
31
|
+
redirect: "follow",
|
|
32
|
+
headers: { "User-Agent": "tingly-box-npx" },
|
|
33
|
+
};
|
|
34
|
+
if (dispatcher) {
|
|
35
|
+
fetchOptions.dispatcher = dispatcher;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const res = await fetch(url, fetchOptions);
|
|
39
|
+
|
|
40
|
+
if (!res.ok) {
|
|
41
|
+
console.error(`❌ Download failed: ${res.status} ${res.statusText}`);
|
|
42
|
+
console.error(` URL: ${url}`);
|
|
43
|
+
throw new Error(`Download failed: ${res.status}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const contentLength = res.headers.get("content-length");
|
|
47
|
+
const totalSize = contentLength ? parseInt(contentLength, 10) : null;
|
|
48
|
+
let downloadedSize = 0;
|
|
49
|
+
|
|
50
|
+
const nodeStream = Readable.fromWeb(res.body);
|
|
51
|
+
const chunks = [];
|
|
52
|
+
|
|
53
|
+
for await (const chunk of nodeStream) {
|
|
54
|
+
chunks.push(chunk);
|
|
55
|
+
downloadedSize += chunk.length;
|
|
56
|
+
if (totalSize) {
|
|
57
|
+
const progress = ((downloadedSize / totalSize) * 100).toFixed(1);
|
|
58
|
+
process.stdout.write(`\r⏱️ ${progressPrefix}: ${progress}% (${formatBytes(downloadedSize)}/${formatBytes(totalSize)})`);
|
|
59
|
+
} else {
|
|
60
|
+
process.stdout.write(`\r⏱️ ${progressPrefix}: ${formatBytes(downloadedSize)}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const zipBuffer = Buffer.concat(chunks);
|
|
65
|
+
|
|
66
|
+
await extractZipBuffer(zipBuffer, extractDir);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Extract ZIP buffer to directory
|
|
71
|
+
* @param {Buffer} zipBuffer - ZIP data
|
|
72
|
+
* @param {string} extractDir - Destination directory
|
|
73
|
+
*/
|
|
74
|
+
export async function extractZipBuffer(zipBuffer, extractDir) {
|
|
75
|
+
console.log(`\n📦 Extracting to ${extractDir}...`);
|
|
76
|
+
|
|
77
|
+
const directory = await unzipper.Open.buffer(zipBuffer);
|
|
78
|
+
|
|
79
|
+
for (const file of directory.files) {
|
|
80
|
+
// Skip metadata
|
|
81
|
+
if (file.type === "Directory" || file.path.startsWith("__MACOSX/") || file.path.includes(".DS_Store")) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const filePath = join(extractDir, file.path);
|
|
86
|
+
const pathParts = file.path.split("/");
|
|
87
|
+
pathParts.pop();
|
|
88
|
+
const fileDir = pathParts.length > 0 ? join(extractDir, ...pathParts) : extractDir;
|
|
89
|
+
|
|
90
|
+
// Create parent directory if needed
|
|
91
|
+
if (fileDir !== extractDir) {
|
|
92
|
+
mkdirSync(fileDir, { recursive: true });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Extract file
|
|
96
|
+
const content = await file.buffer();
|
|
97
|
+
const fileStream = createWriteStream(filePath);
|
|
98
|
+
await new Promise((resolve, reject) => {
|
|
99
|
+
fileStream.write(content, (err) => {
|
|
100
|
+
if (err) reject(err);
|
|
101
|
+
else {
|
|
102
|
+
fileStream.end();
|
|
103
|
+
resolve();
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Set permissions
|
|
109
|
+
if (process.platform !== "win32" && file.unixPermissions && file.unixPermissions > 0) {
|
|
110
|
+
chmodSync(filePath, file.unixPermissions);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
console.log(`✅ Extracted successfully`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Recursively find binary in directory
|
|
119
|
+
* @param {string} dir - Directory to search
|
|
120
|
+
* @param {string} binaryName - Binary name to find
|
|
121
|
+
* @returns {string|null} Binary path or null
|
|
122
|
+
*/
|
|
123
|
+
export function findBinary(dir, binaryName) {
|
|
124
|
+
const { readdirSync, statSync } = await import("fs");
|
|
125
|
+
const entries = readdirSync(dir);
|
|
126
|
+
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
const fullPath = join(dir, entry);
|
|
129
|
+
try {
|
|
130
|
+
const stat = statSync(fullPath);
|
|
131
|
+
if (stat.isFile() && entry === binaryName) {
|
|
132
|
+
return fullPath;
|
|
133
|
+
}
|
|
134
|
+
if (stat.isDirectory()) {
|
|
135
|
+
const found = findBinary(fullPath, binaryName);
|
|
136
|
+
if (found) return found;
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Format bytes to human-readable string
|
|
147
|
+
* @param {number} bytes - Number of bytes
|
|
148
|
+
* @returns {string} Formatted string
|
|
149
|
+
*/
|
|
150
|
+
export function formatBytes(bytes) {
|
|
151
|
+
if (bytes === 0) return "0 B";
|
|
152
|
+
const k = 1024;
|
|
153
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
154
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
155
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i];
|
|
156
|
+
}
|
package/lib/executor.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Binary and app execution
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
6
|
+
import { existsSync, chmodSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Execute binary with arguments
|
|
11
|
+
* @param {string} binaryPath - Path to binary
|
|
12
|
+
* @param {string[]} args - Command line arguments
|
|
13
|
+
* @param {string[]} defaultArgs - Default arguments if none provided
|
|
14
|
+
*/
|
|
15
|
+
export function executeBinary(binaryPath, args = [], defaultArgs = []) {
|
|
16
|
+
console.log(`🔍 Executing: ${binaryPath}`);
|
|
17
|
+
|
|
18
|
+
const argsToUse = args.length > 0 ? args : defaultArgs;
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
execFileSync(binaryPath, argsToUse, {
|
|
22
|
+
stdio: "inherit",
|
|
23
|
+
encoding: "utf8",
|
|
24
|
+
});
|
|
25
|
+
} catch (execError) {
|
|
26
|
+
handleExecutionError(execError, binaryPath);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Launch macOS app bundle
|
|
32
|
+
* @param {string} appPath - Path to .app bundle
|
|
33
|
+
*/
|
|
34
|
+
export function launchApp(appPath) {
|
|
35
|
+
console.log(`🔍 Launching: ${appPath}`);
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
// Sign the app first
|
|
39
|
+
console.log(`🔐 Signing app...`);
|
|
40
|
+
execFileSync("codesign", ["--force", "--deep", "--sign", "-", appPath], {
|
|
41
|
+
stdio: "inherit",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
console.log(`🚀 Launching app...`);
|
|
45
|
+
execFileSync("open", ["-a", appPath], {
|
|
46
|
+
stdio: "inherit",
|
|
47
|
+
});
|
|
48
|
+
console.log(`✅ App launched!`);
|
|
49
|
+
} catch (execError) {
|
|
50
|
+
handleExecutionError(execError, appPath);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Handle execution errors
|
|
56
|
+
* @param {Error} execError - Execution error
|
|
57
|
+
* @param {string} binaryPath - Binary or app path
|
|
58
|
+
*/
|
|
59
|
+
function handleExecutionError(execError, binaryPath) {
|
|
60
|
+
console.error(`\n❌ Execution failed`);
|
|
61
|
+
console.error(`┌─ Error Details:`);
|
|
62
|
+
console.error(`│ Message: ${execError.message}`);
|
|
63
|
+
|
|
64
|
+
if (execError.code) {
|
|
65
|
+
console.error(`│ Code: ${execError.code}`);
|
|
66
|
+
switch (execError.code) {
|
|
67
|
+
case "ENOENT":
|
|
68
|
+
console.error(`│ └─ Binary not found at: ${binaryPath}`);
|
|
69
|
+
console.error(`│ Try clearing cache and reinstalling`);
|
|
70
|
+
break;
|
|
71
|
+
case "EACCES":
|
|
72
|
+
console.error(`│ └─ Permission denied`);
|
|
73
|
+
console.error(`│ Try: chmod +x "${binaryPath}"`);
|
|
74
|
+
break;
|
|
75
|
+
case "ETXTBSY":
|
|
76
|
+
console.error(`│ └─ Binary file is busy`);
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (execError.status !== null && execError.status !== undefined) {
|
|
82
|
+
console.error(`│ Exit Code: ${execError.status}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (execError.signal) {
|
|
86
|
+
console.error(`│ Signal: ${execError.signal}`);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
console.error(`└─ Path: ${binaryPath}`);
|
|
90
|
+
console.error(` Platform: ${process.platform} (${process.arch})`);
|
|
91
|
+
|
|
92
|
+
const exitCode = execError.status !== undefined ? execError.status : 1;
|
|
93
|
+
process.exit(exitCode);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Ensure binary is executable
|
|
98
|
+
* @param {string} binaryPath - Path to binary
|
|
99
|
+
*/
|
|
100
|
+
export function ensureExecutable(binaryPath) {
|
|
101
|
+
if (process.platform !== "win32" && existsSync(binaryPath)) {
|
|
102
|
+
try {
|
|
103
|
+
chmodSync(binaryPath, 0o755);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
console.warn(`⚠️ Failed to set executable permission: ${e.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Ensure app bundle is properly signed and executable
|
|
112
|
+
* @param {string} appPath - Path to .app bundle
|
|
113
|
+
*/
|
|
114
|
+
export function ensureAppExecutable(appPath) {
|
|
115
|
+
if (!existsSync(appPath)) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Ensure binary inside app is executable
|
|
120
|
+
const appBinaryPath = join(appPath, "Contents", "MacOS", "tingly-box");
|
|
121
|
+
ensureExecutable(appBinaryPath);
|
|
122
|
+
}
|
package/lib/platform.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform detection and utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get platform information
|
|
7
|
+
* @returns {Object} Platform info
|
|
8
|
+
*/
|
|
9
|
+
export function getPlatformInfo() {
|
|
10
|
+
const platform = process.platform;
|
|
11
|
+
const arch = process.arch;
|
|
12
|
+
|
|
13
|
+
let platformDir;
|
|
14
|
+
let archDir;
|
|
15
|
+
let suffix = "";
|
|
16
|
+
|
|
17
|
+
if (platform === "darwin") {
|
|
18
|
+
platformDir = "macos";
|
|
19
|
+
if (arch === "arm64") archDir = "arm64";
|
|
20
|
+
else archDir = "amd64";
|
|
21
|
+
} else if (platform === "linux") {
|
|
22
|
+
platformDir = "linux";
|
|
23
|
+
if (arch === "x64") archDir = "amd64";
|
|
24
|
+
else if (arch === "arm64") archDir = "arm64";
|
|
25
|
+
else archDir = arch;
|
|
26
|
+
} else if (platform === "win32") {
|
|
27
|
+
platformDir = "windows";
|
|
28
|
+
if (arch === "x64") archDir = "amd64";
|
|
29
|
+
else if (arch === "ia32") archDir = "386";
|
|
30
|
+
else archDir = arch;
|
|
31
|
+
suffix = ".exe";
|
|
32
|
+
} else {
|
|
33
|
+
throw new Error(`Unsupported platform: ${platform}/${arch}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return { platform, arch, platformDir, archDir, suffix };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Get platform identifier for package naming
|
|
41
|
+
* @returns {string} Platform identifier (e.g., "darwin-arm64")
|
|
42
|
+
*/
|
|
43
|
+
export function getPlatformIdentifier() {
|
|
44
|
+
const info = getPlatformInfo();
|
|
45
|
+
const npmArch = info.arch === "x64" ? "amd64" : info.arch;
|
|
46
|
+
return `${info.platform}-${npmArch}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get npm package name for platform
|
|
51
|
+
* @returns {string} Package name (e.g., "tingly-box-darwin")
|
|
52
|
+
*/
|
|
53
|
+
export function getPlatformPackageName() {
|
|
54
|
+
const info = getPlatformInfo();
|
|
55
|
+
return `tingly-box-${info.platform}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Get binary name for current platform
|
|
60
|
+
* @param {string} baseName - Base binary name
|
|
61
|
+
* @returns {string} Full binary name with extension
|
|
62
|
+
*/
|
|
63
|
+
export function getBinaryName(baseName = "tingly-box") {
|
|
64
|
+
return baseName + (process.platform === "win32" ? ".exe" : "");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if platform is supported
|
|
69
|
+
* @param {string[]} supportedPlatforms - List of supported platforms
|
|
70
|
+
* @returns {boolean}
|
|
71
|
+
*/
|
|
72
|
+
export function isPlatformSupported(supportedPlatforms) {
|
|
73
|
+
const info = getPlatformInfo();
|
|
74
|
+
return supportedPlatforms.includes(info.platform);
|
|
75
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "tingly-box-bundle",
|
|
3
|
+
"version": "0.0.37-dev",
|
|
4
|
+
"description": "Tingly-Box AI gateway CLI with bundled binaries (offline-ready)",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"ai",
|
|
7
|
+
"gateway",
|
|
8
|
+
"openai",
|
|
9
|
+
"anthropic",
|
|
10
|
+
"claude",
|
|
11
|
+
"cli",
|
|
12
|
+
"tingly",
|
|
13
|
+
"offline"
|
|
14
|
+
],
|
|
15
|
+
"homepage": "https://github.com/tingly-dev/tingly-box",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/tingly-dev/tingly-box.git"
|
|
19
|
+
},
|
|
20
|
+
"license": "MPL-2.0",
|
|
21
|
+
"author": "Tingly Dev",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=18.0.0"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"tingly-box-bundle": "bin.js"
|
|
30
|
+
},
|
|
31
|
+
"type": "module",
|
|
32
|
+
"files": [
|
|
33
|
+
"bin.js",
|
|
34
|
+
"lib/"
|
|
35
|
+
],
|
|
36
|
+
"os": [
|
|
37
|
+
"darwin",
|
|
38
|
+
"linux",
|
|
39
|
+
"win32"
|
|
40
|
+
],
|
|
41
|
+
"cpu": [
|
|
42
|
+
"x64",
|
|
43
|
+
"arm64"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@tingly-box/shared": "workspace:*"
|
|
47
|
+
},
|
|
48
|
+
"optionalDependencies": {
|
|
49
|
+
"tingly-box-darwin": "^1.6.1",
|
|
50
|
+
"tingly-box-linux": "^1.6.1",
|
|
51
|
+
"tingly-box-win32": "^1.6.1"
|
|
52
|
+
}
|
|
53
|
+
}
|