openteams-cli 0.3.7

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.
Files changed (3) hide show
  1. package/bin/cli.js +314 -0
  2. package/bin/download.js +234 -0
  3. package/package.json +44 -0
package/bin/cli.js ADDED
@@ -0,0 +1,314 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { execSync, spawn } = require("child_process");
4
+ const fs = require("fs");
5
+ const os = require("os");
6
+ const path = require("path");
7
+ const AdmZip = require("adm-zip");
8
+
9
+ const {
10
+ ensureBinary,
11
+ BINARY_TAG,
12
+ CACHE_DIR,
13
+ LOCAL_DEV_MODE,
14
+ LOCAL_DIST_DIR,
15
+ resolveRemoteSource,
16
+ } = require("./download");
17
+
18
+ const CLI_VERSION = require("../package.json").version;
19
+
20
+ const APP_NAME = "openteams-cli";
21
+ const INSTALL_DIR = path.join(os.homedir(), ".openteams");
22
+ const BIN_DIR = path.join(INSTALL_DIR, "bin");
23
+
24
+ function printInfo(message) {
25
+ console.log(` ${message}`);
26
+ }
27
+
28
+ function printStep(step, message) {
29
+ console.log(` [${step}] ${message}`);
30
+ }
31
+
32
+ function printSuccess(message) {
33
+ console.log(` OK ${message}`);
34
+ }
35
+
36
+ function printError(message) {
37
+ console.error(` ERROR ${message}`);
38
+ }
39
+
40
+ function getEffectiveArch() {
41
+ const platform = process.platform;
42
+ const nodeArch = process.arch;
43
+
44
+ if (platform === "darwin") {
45
+ if (nodeArch === "arm64") return "arm64";
46
+
47
+ try {
48
+ const translated = execSync("sysctl -in sysctl.proc_translated", {
49
+ encoding: "utf8",
50
+ }).trim();
51
+ if (translated === "1") return "arm64";
52
+ } catch (_err) {}
53
+
54
+ return "x64";
55
+ }
56
+
57
+ if (/arm/i.test(nodeArch)) {
58
+ return "arm64";
59
+ }
60
+
61
+ if (platform === "win32") {
62
+ const architecture = process.env.PROCESSOR_ARCHITECTURE || "";
63
+ const wow64 = process.env.PROCESSOR_ARCHITEW6432 || "";
64
+ if (/arm/i.test(architecture) || /arm/i.test(wow64)) {
65
+ return "arm64";
66
+ }
67
+ }
68
+
69
+ return "x64";
70
+ }
71
+
72
+ function getPlatformTarget() {
73
+ const platform = process.platform;
74
+ const arch = getEffectiveArch();
75
+
76
+ if (platform === "linux" && arch === "x64") {
77
+ return { platformDir: "linux-x64", note: null };
78
+ }
79
+
80
+ if (platform === "linux" && arch === "arm64") {
81
+ return { platformDir: "linux-arm64", note: null };
82
+ }
83
+
84
+ if (platform === "darwin" && arch === "x64") {
85
+ return { platformDir: "macos-x64", note: null };
86
+ }
87
+
88
+ if (platform === "darwin" && arch === "arm64") {
89
+ return { platformDir: "macos-arm64", note: null };
90
+ }
91
+
92
+ if (platform === "win32" && arch === "x64") {
93
+ return { platformDir: "windows-x64", note: null };
94
+ }
95
+
96
+ if (platform === "win32" && arch === "arm64") {
97
+ return {
98
+ platformDir: "windows-x64",
99
+ note:
100
+ "Windows ARM64 binary is not published yet. Falling back to windows-x64 binary.",
101
+ };
102
+ }
103
+
104
+ printError(`Unsupported platform: ${platform}-${arch}`);
105
+ printError("Supported targets: linux-x64, linux-arm64, macos-x64, macos-arm64, windows-x64");
106
+ process.exit(1);
107
+ }
108
+
109
+ function getBinaryName() {
110
+ return process.platform === "win32" ? `${APP_NAME}.exe` : APP_NAME;
111
+ }
112
+
113
+ function getInstalledBinaryPath() {
114
+ return path.join(BIN_DIR, getBinaryName());
115
+ }
116
+
117
+ function showProgress(downloaded, total) {
118
+ const percent = total > 0 ? Math.round((downloaded / total) * 100) : 0;
119
+ const downloadedMb = (downloaded / (1024 * 1024)).toFixed(1);
120
+ const totalMb = total > 0 ? (total / (1024 * 1024)).toFixed(1) : "?";
121
+ process.stderr.write(
122
+ `\r Downloading: ${downloadedMb}MB / ${totalMb}MB (${percent}%)`,
123
+ );
124
+ }
125
+
126
+ function extractBinary(zipPath) {
127
+ fs.mkdirSync(BIN_DIR, { recursive: true });
128
+
129
+ const expectedBinaryName = getBinaryName();
130
+ const expectedPath = path.join(BIN_DIR, expectedBinaryName);
131
+
132
+ try {
133
+ if (fs.existsSync(expectedPath)) {
134
+ fs.unlinkSync(expectedPath);
135
+ }
136
+ } catch (_err) {}
137
+
138
+ const zip = new AdmZip(zipPath);
139
+ zip.extractAllTo(BIN_DIR, true);
140
+
141
+ let binaryPath = expectedPath;
142
+ if (!fs.existsSync(binaryPath)) {
143
+ const candidates = fs
144
+ .readdirSync(BIN_DIR)
145
+ .filter((name) =>
146
+ [expectedBinaryName, `${expectedBinaryName}.exe`].includes(name),
147
+ );
148
+
149
+ if (candidates.length > 0) {
150
+ binaryPath = path.join(BIN_DIR, candidates[0]);
151
+ }
152
+ }
153
+
154
+ if (!fs.existsSync(binaryPath)) {
155
+ throw new Error(
156
+ `Extracted binary not found in ${BIN_DIR}. The archive may be invalid.`,
157
+ );
158
+ }
159
+
160
+ if (process.platform !== "win32") {
161
+ try {
162
+ fs.chmodSync(binaryPath, 0o755);
163
+ } catch (_err) {}
164
+ }
165
+
166
+ return binaryPath;
167
+ }
168
+
169
+ async function installBinary(options = {}) {
170
+ const { force = false } = options;
171
+ const target = getPlatformTarget();
172
+
173
+ if (target.note) {
174
+ console.log(` WARN ${target.note}`);
175
+ }
176
+
177
+ printStep("1/2", "Preparing binary package...");
178
+ fs.mkdirSync(INSTALL_DIR, { recursive: true });
179
+
180
+ if (force) {
181
+ try {
182
+ fs.rmSync(path.join(CACHE_DIR, BINARY_TAG, target.platformDir, `${APP_NAME}.zip`), { force: true });
183
+ fs.rmSync(getInstalledBinaryPath(), { force: true });
184
+ } catch (_err) {}
185
+ }
186
+
187
+ printStep("2/2", "Downloading prebuilt binary...");
188
+ let zipPath;
189
+ try {
190
+ zipPath = await ensureBinary(target.platformDir, showProgress);
191
+ if (!LOCAL_DEV_MODE) {
192
+ process.stderr.write("\n");
193
+ }
194
+ } catch (err) {
195
+ process.stderr.write("\n");
196
+ throw new Error(`Download failed: ${err.message}`);
197
+ }
198
+
199
+ const binaryPath = extractBinary(zipPath);
200
+
201
+ printSuccess(`Installed ${APP_NAME} to ${binaryPath}`);
202
+ return binaryPath;
203
+ }
204
+
205
+ function isInstalled() {
206
+ return fs.existsSync(getInstalledBinaryPath());
207
+ }
208
+
209
+ async function ensureInstalled() {
210
+ if (isInstalled()) {
211
+ return getInstalledBinaryPath();
212
+ }
213
+
214
+ printInfo("No local installation found. Installing now...");
215
+ return installBinary();
216
+ }
217
+
218
+ function launchBinary(binaryPath, args) {
219
+ return new Promise((resolve, reject) => {
220
+ const child = spawn(binaryPath, args, {
221
+ stdio: "inherit",
222
+ env: process.env,
223
+ });
224
+
225
+ child.on("error", (err) => reject(err));
226
+ child.on("exit", (code) => resolve(code || 0));
227
+
228
+ process.on("SIGINT", () => {
229
+ child.kill("SIGINT");
230
+ });
231
+ process.on("SIGTERM", () => {
232
+ child.kill("SIGTERM");
233
+ });
234
+ });
235
+ }
236
+
237
+ function showHelp() {
238
+ console.log(`
239
+ Usage: npx openteams-cli [command] [args]
240
+
241
+ Commands:
242
+ install Download and install prebuilt binary only
243
+ update Force re-download and reinstall
244
+ --help, -h Show help
245
+ --version Show CLI version
246
+
247
+ Default behavior:
248
+ npx openteams-cli
249
+ -> install (if needed) + run CLI
250
+
251
+ Pass-through args:
252
+ npx openteams-cli -- --help
253
+ `);
254
+ }
255
+
256
+ function checkNodeVersion() {
257
+ const major = Number.parseInt(process.versions.node.split(".")[0], 10);
258
+ if (Number.isNaN(major) || major < 18) {
259
+ printError(`Node.js 18+ is required, found v${process.versions.node}`);
260
+ process.exit(1);
261
+ }
262
+ }
263
+
264
+ async function main() {
265
+ checkNodeVersion();
266
+
267
+ const rawArgs = process.argv.slice(2);
268
+ const args = rawArgs[0] === "--" ? rawArgs.slice(1) : rawArgs;
269
+ const command = args[0];
270
+
271
+ if (command === "--help" || command === "-h") {
272
+ showHelp();
273
+ return;
274
+ }
275
+
276
+ if (command === "--version" || command === "-v") {
277
+ console.log(`${APP_NAME} v${CLI_VERSION}`);
278
+ return;
279
+ }
280
+
281
+ if (command === "install") {
282
+ await installBinary();
283
+ console.log("");
284
+ printInfo("Run `npx openteams-cli` to start.");
285
+ return;
286
+ }
287
+
288
+ if (command === "update") {
289
+ await installBinary({ force: true });
290
+ console.log("");
291
+ printInfo("Update completed.");
292
+ return;
293
+ }
294
+
295
+ let runArgs = args;
296
+ if (command === "start") {
297
+ runArgs = args.slice(1);
298
+ }
299
+
300
+ console.log("");
301
+ const binaryPath = await ensureInstalled();
302
+ console.log("");
303
+
304
+ const exitCode = await launchBinary(binaryPath, runArgs);
305
+ process.exit(exitCode);
306
+ }
307
+
308
+ main().catch((err) => {
309
+ printError(err.message || String(err));
310
+ if (process.env.OPENTEAMS_DEBUG === "1") {
311
+ console.error(err.stack || err);
312
+ }
313
+ process.exit(1);
314
+ });
@@ -0,0 +1,234 @@
1
+ const https = require("https");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+ const os = require("os");
5
+ const crypto = require("crypto");
6
+
7
+ const DEFAULT_OSS_BASE_URL = "__OSS_PUBLIC_URL__";
8
+ const DEFAULT_R2_BASE_URL = "https://pub-443f2c6e108c4de4a8c0dadf2c6e06cc.r2.dev";
9
+ const DEFAULT_BINARY_TAG = "v0.3.7-20260323111520";
10
+
11
+ const OSS_BASE_URL = normalizeBaseUrl(
12
+ process.env.OPENTEAMS_OSS_BASE_URL || DEFAULT_OSS_BASE_URL,
13
+ );
14
+ const R2_BASE_URL = normalizeBaseUrl(
15
+ process.env.OPENTEAMS_R2_BASE_URL || DEFAULT_R2_BASE_URL,
16
+ );
17
+ const BINARY_TAG =
18
+ process.env.OPENTEAMS_BINARY_TAG || DEFAULT_BINARY_TAG;
19
+
20
+ const INSTALL_DIR = path.join(os.homedir(), ".openteams");
21
+ const CACHE_DIR = path.join(INSTALL_DIR, "cache");
22
+
23
+ const LOCAL_DIST_DIR = path.join(__dirname, "..", "dist");
24
+ const LOCAL_DEV_MODE =
25
+ fs.existsSync(LOCAL_DIST_DIR) || process.env.OPENTEAMS_LOCAL === "1";
26
+
27
+ function normalizeBaseUrl(url) {
28
+ if (!url) return "";
29
+ return url.replace(/\/+$/, "");
30
+ }
31
+
32
+ function isUnresolvedTemplateToken(value) {
33
+ if (!value) return true;
34
+ return /^__[A-Z0-9_]+__$/.test(String(value).trim());
35
+ }
36
+
37
+ function isConfiguredBaseUrl(url) {
38
+ return Boolean(url) && !isUnresolvedTemplateToken(url);
39
+ }
40
+
41
+ function resolveRemoteSource() {
42
+ if (isConfiguredBaseUrl(OSS_BASE_URL)) {
43
+ return {
44
+ provider: "oss",
45
+ baseUrl: OSS_BASE_URL,
46
+ };
47
+ }
48
+
49
+ if (isConfiguredBaseUrl(R2_BASE_URL)) {
50
+ return {
51
+ provider: "r2",
52
+ baseUrl: R2_BASE_URL,
53
+ };
54
+ }
55
+
56
+ return null;
57
+ }
58
+
59
+ function ensureRemoteConfig() {
60
+ if (LOCAL_DEV_MODE) return;
61
+
62
+ const source = resolveRemoteSource();
63
+ if (!source) {
64
+ throw new Error(
65
+ "Binary source URL is not configured. Set OPENTEAMS_OSS_BASE_URL or OPENTEAMS_R2_BASE_URL, or publish npm package with URL injection.",
66
+ );
67
+ }
68
+
69
+ if (isUnresolvedTemplateToken(BINARY_TAG)) {
70
+ throw new Error(
71
+ "Binary tag is not configured. The npm package was published without binary tag injection.",
72
+ );
73
+ }
74
+
75
+ return source;
76
+ }
77
+
78
+ function fetchJson(url) {
79
+ return new Promise((resolve, reject) => {
80
+ https
81
+ .get(url, (res) => {
82
+ if (res.statusCode === 301 || res.statusCode === 302) {
83
+ return fetchJson(res.headers.location).then(resolve).catch(reject);
84
+ }
85
+
86
+ if (res.statusCode !== 200) {
87
+ return reject(new Error(`HTTP ${res.statusCode} while fetching ${url}`));
88
+ }
89
+
90
+ let body = "";
91
+ res.on("data", (chunk) => {
92
+ body += chunk;
93
+ });
94
+ res.on("end", () => {
95
+ try {
96
+ resolve(JSON.parse(body));
97
+ } catch (_err) {
98
+ reject(new Error(`Invalid JSON response from ${url}`));
99
+ }
100
+ });
101
+ })
102
+ .on("error", reject);
103
+ });
104
+ }
105
+
106
+ function downloadFile(url, destinationPath, expectedSha256, onProgress) {
107
+ const tempPath = `${destinationPath}.tmp`;
108
+
109
+ return new Promise((resolve, reject) => {
110
+ const file = fs.createWriteStream(tempPath);
111
+ const hash = crypto.createHash("sha256");
112
+
113
+ const cleanup = () => {
114
+ try {
115
+ fs.unlinkSync(tempPath);
116
+ } catch (_err) {}
117
+ };
118
+
119
+ https
120
+ .get(url, (res) => {
121
+ if (res.statusCode === 301 || res.statusCode === 302) {
122
+ file.close();
123
+ cleanup();
124
+ return downloadFile(
125
+ res.headers.location,
126
+ destinationPath,
127
+ expectedSha256,
128
+ onProgress,
129
+ )
130
+ .then(resolve)
131
+ .catch(reject);
132
+ }
133
+
134
+ if (res.statusCode !== 200) {
135
+ file.close();
136
+ cleanup();
137
+ return reject(new Error(`HTTP ${res.statusCode} while downloading ${url}`));
138
+ }
139
+
140
+ const totalSize = Number.parseInt(res.headers["content-length"], 10);
141
+ let downloadedSize = 0;
142
+
143
+ res.on("data", (chunk) => {
144
+ downloadedSize += chunk.length;
145
+ hash.update(chunk);
146
+ if (onProgress) {
147
+ onProgress(downloadedSize, Number.isFinite(totalSize) ? totalSize : 0);
148
+ }
149
+ });
150
+
151
+ res.pipe(file);
152
+
153
+ file.on("finish", () => {
154
+ file.close();
155
+
156
+ const actualSha256 = hash.digest("hex");
157
+ if (expectedSha256 && actualSha256 !== expectedSha256) {
158
+ cleanup();
159
+ return reject(
160
+ new Error(
161
+ `Checksum mismatch, expected ${expectedSha256}, got ${actualSha256}`,
162
+ ),
163
+ );
164
+ }
165
+
166
+ try {
167
+ fs.renameSync(tempPath, destinationPath);
168
+ resolve(destinationPath);
169
+ } catch (err) {
170
+ cleanup();
171
+ reject(err);
172
+ }
173
+ });
174
+ })
175
+ .on("error", (err) => {
176
+ file.close();
177
+ cleanup();
178
+ reject(err);
179
+ });
180
+ });
181
+ }
182
+
183
+ async function ensureBinary(platform, onProgress) {
184
+ const binaryName = "openteams-cli";
185
+
186
+ if (LOCAL_DEV_MODE) {
187
+ const localZipPath = path.join(LOCAL_DIST_DIR, platform, `${binaryName}.zip`);
188
+ if (fs.existsSync(localZipPath)) {
189
+ return localZipPath;
190
+ }
191
+
192
+ throw new Error(
193
+ `Local binary not found: ${localZipPath}\nRun your local binary packaging first.`,
194
+ );
195
+ }
196
+
197
+ const source = ensureRemoteConfig();
198
+
199
+ const platformCacheDir = path.join(CACHE_DIR, BINARY_TAG, platform);
200
+ const zipPath = path.join(platformCacheDir, `${binaryName}.zip`);
201
+
202
+ if (fs.existsSync(zipPath)) {
203
+ return zipPath;
204
+ }
205
+
206
+ fs.mkdirSync(platformCacheDir, { recursive: true });
207
+
208
+ const manifest = await fetchJson(
209
+ `${source.baseUrl}/binaries/${BINARY_TAG}/manifest.json`,
210
+ );
211
+ const binaryInfo = manifest.platforms?.[platform]?.[binaryName];
212
+
213
+ if (!binaryInfo) {
214
+ throw new Error(
215
+ `Binary ${binaryName} is not available for platform ${platform} in tag ${BINARY_TAG}.`,
216
+ );
217
+ }
218
+
219
+ const binaryUrl = `${source.baseUrl}/binaries/${BINARY_TAG}/${platform}/${binaryName}.zip`;
220
+ await downloadFile(binaryUrl, zipPath, binaryInfo.sha256, onProgress);
221
+
222
+ return zipPath;
223
+ }
224
+
225
+ module.exports = {
226
+ OSS_BASE_URL,
227
+ R2_BASE_URL,
228
+ BINARY_TAG,
229
+ CACHE_DIR,
230
+ LOCAL_DEV_MODE,
231
+ LOCAL_DIST_DIR,
232
+ resolveRemoteSource,
233
+ ensureBinary,
234
+ };
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "openteams-cli",
3
+ "version": "0.3.7",
4
+ "description": "OpenTeams CLI - AI-powered development tool",
5
+ "author": "openteams-lab",
6
+ "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/openteams-lab/openteams"
10
+ },
11
+ "homepage": "https://github.com/openteams-lab/openteams",
12
+ "bugs": {
13
+ "url": "https://github.com/openteams-lab/openteams/issues"
14
+ },
15
+ "keywords": [
16
+ "ai",
17
+ "cli",
18
+ "coding",
19
+ "assistant",
20
+ "agent"
21
+ ],
22
+ "bin": {
23
+ "openteams-cli": "bin/cli.js"
24
+ },
25
+ "main": "bin/cli.js",
26
+ "files": [
27
+ "bin"
28
+ ],
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "os": [
33
+ "darwin",
34
+ "linux",
35
+ "win32"
36
+ ],
37
+ "cpu": [
38
+ "x64",
39
+ "arm64"
40
+ ],
41
+ "dependencies": {
42
+ "adm-zip": "^0.5.16"
43
+ }
44
+ }