aether-init 0.4.0

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 (2) hide show
  1. package/index.mjs +312 -0
  2. package/package.json +17 -0
package/index.mjs ADDED
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * aether-init: One-command installer for Aether
5
+ *
6
+ * Usage: npx aether-init
7
+ *
8
+ * Does:
9
+ * 1. Downloads Aether Bridge binary for current OS/arch
10
+ * 2. Installs Unity SDK via UPM (if in a Unity project)
11
+ * 3. Writes .cursor/mcp.json so Cursor connects automatically
12
+ */
13
+
14
+ import { createWriteStream, existsSync, mkdirSync, readFileSync, writeFileSync, chmodSync } from "fs";
15
+ import { homedir, platform, arch } from "os";
16
+ import { join, dirname } from "path";
17
+ import { execSync } from "child_process";
18
+ import { get as httpsGet } from "https";
19
+ import { get as httpGet } from "http";
20
+
21
+ // =============================================================================
22
+ // Constants
23
+ // =============================================================================
24
+
25
+ const DOWNLOADS_BASE = "https://downloads.getaether.dev";
26
+ const UPM_GIT_URL = "https://github.com/finexma-dev/aether.git?path=unity-sdk/Aether";
27
+
28
+ // =============================================================================
29
+ // Utilities
30
+ // =============================================================================
31
+
32
+ function log(msg) {
33
+ console.log(`\x1b[36m[aether]\x1b[0m ${msg}`);
34
+ }
35
+
36
+ function success(msg) {
37
+ console.log(`\x1b[32m[aether]\x1b[0m ${msg}`);
38
+ }
39
+
40
+ function warn(msg) {
41
+ console.log(`\x1b[33m[aether]\x1b[0m ${msg}`);
42
+ }
43
+
44
+ function error(msg) {
45
+ console.error(`\x1b[31m[aether]\x1b[0m ${msg}`);
46
+ }
47
+
48
+ function detectPlatform() {
49
+ const os = platform();
50
+ const cpu = arch();
51
+
52
+ if (os === "win32") return { os: "windows", arch: "x64", ext: ".exe" };
53
+ if (os === "darwin") {
54
+ // arm64 = Apple Silicon, x64 = Intel
55
+ return { os: "darwin", arch: cpu === "arm64" ? "arm64" : "x64", ext: "" };
56
+ }
57
+ if (os === "linux") return { os: "linux", arch: "x64", ext: "" };
58
+
59
+ throw new Error(`Unsupported platform: ${os}/${cpu}`);
60
+ }
61
+
62
+ function getBinaryName(plat) {
63
+ return `aether-bridge-${plat.os}-${plat.arch}${plat.ext}`;
64
+ }
65
+
66
+ function getInstallDir() {
67
+ return join(homedir(), ".aether", "bin");
68
+ }
69
+
70
+ function fetchJson(url) {
71
+ return new Promise((resolve, reject) => {
72
+ const getter = url.startsWith("https") ? httpsGet : httpGet;
73
+ getter(url, (res) => {
74
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
75
+ return fetchJson(res.headers.location).then(resolve, reject);
76
+ }
77
+ if (res.statusCode !== 200) {
78
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
79
+ }
80
+ let data = "";
81
+ res.on("data", (chunk) => (data += chunk));
82
+ res.on("end", () => {
83
+ try {
84
+ resolve(JSON.parse(data));
85
+ } catch (e) {
86
+ reject(new Error(`Invalid JSON from ${url}`));
87
+ }
88
+ });
89
+ res.on("error", reject);
90
+ }).on("error", reject);
91
+ });
92
+ }
93
+
94
+ function downloadFile(url, dest) {
95
+ return new Promise((resolve, reject) => {
96
+ const getter = url.startsWith("https") ? httpsGet : httpGet;
97
+ getter(url, (res) => {
98
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
99
+ return downloadFile(res.headers.location, dest).then(resolve, reject);
100
+ }
101
+ if (res.statusCode !== 200) {
102
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
103
+ }
104
+ const file = createWriteStream(dest);
105
+ res.pipe(file);
106
+ file.on("finish", () => {
107
+ file.close(resolve);
108
+ });
109
+ file.on("error", (e) => {
110
+ file.close();
111
+ reject(e);
112
+ });
113
+ }).on("error", reject);
114
+ });
115
+ }
116
+
117
+ // =============================================================================
118
+ // Steps
119
+ // =============================================================================
120
+
121
+ async function downloadBridge() {
122
+ const plat = detectPlatform();
123
+ const binaryName = getBinaryName(plat);
124
+ const installDir = getInstallDir();
125
+ const destPath = join(installDir, binaryName);
126
+
127
+ log(`Detected platform: ${plat.os}/${plat.arch}`);
128
+
129
+ // Create install directory
130
+ mkdirSync(installDir, { recursive: true });
131
+
132
+ // Fetch latest.json to get download URL
133
+ log("Fetching latest release info...");
134
+ let downloadUrl;
135
+ try {
136
+ const latest = await fetchJson(`${DOWNLOADS_BASE}/releases/latest.json`);
137
+ const keyMap = {
138
+ "windows-x64": "windowsX64",
139
+ "darwin-arm64": "macosArm64",
140
+ "darwin-x64": "macosX64",
141
+ "linux-x64": "linuxX64",
142
+ };
143
+ const key = keyMap[`${plat.os}-${plat.arch}`];
144
+ downloadUrl = latest?.files?.bridge?.[key]?.url;
145
+
146
+ if (!downloadUrl) {
147
+ // Fallback to direct URL pattern
148
+ const version = latest?.version || "latest";
149
+ downloadUrl = `${DOWNLOADS_BASE}/releases/v${version}/${binaryName}`;
150
+ }
151
+ } catch {
152
+ // Fallback: try direct download
153
+ downloadUrl = `${DOWNLOADS_BASE}/releases/latest/${binaryName}`;
154
+ }
155
+
156
+ log(`Downloading ${binaryName}...`);
157
+ await downloadFile(downloadUrl, destPath);
158
+
159
+ // Make executable on Unix
160
+ if (plat.os !== "windows") {
161
+ chmodSync(destPath, 0o755);
162
+ }
163
+
164
+ // Remove macOS quarantine
165
+ if (plat.os === "darwin") {
166
+ try {
167
+ execSync(`xattr -d com.apple.quarantine "${destPath}" 2>/dev/null`, { stdio: "ignore" });
168
+ } catch {
169
+ // Quarantine may not be present, that's fine
170
+ }
171
+ }
172
+
173
+ success(`Bridge installed: ${destPath}`);
174
+ return destPath;
175
+ }
176
+
177
+ function findUnityProject() {
178
+ // Look for Assets/ directory in cwd or parent directories
179
+ let dir = process.cwd();
180
+ for (let i = 0; i < 5; i++) {
181
+ if (existsSync(join(dir, "Assets")) && existsSync(join(dir, "ProjectSettings"))) {
182
+ return dir;
183
+ }
184
+ const parent = dirname(dir);
185
+ if (parent === dir) break;
186
+ dir = parent;
187
+ }
188
+ return null;
189
+ }
190
+
191
+ function installUnitySdk(projectDir) {
192
+ const manifestPath = join(projectDir, "Packages", "manifest.json");
193
+
194
+ if (!existsSync(manifestPath)) {
195
+ warn("No Packages/manifest.json found. Skipping UPM install.");
196
+ warn(`Manually add the SDK: Window > Package Manager > + > Add from git URL:`);
197
+ warn(` ${UPM_GIT_URL}`);
198
+ return false;
199
+ }
200
+
201
+ try {
202
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
203
+
204
+ if (manifest.dependencies && manifest.dependencies["dev.getaether.sdk"]) {
205
+ log("Aether SDK already in manifest.json");
206
+ return true;
207
+ }
208
+
209
+ if (!manifest.dependencies) manifest.dependencies = {};
210
+ manifest.dependencies["dev.getaether.sdk"] = UPM_GIT_URL;
211
+
212
+ writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n");
213
+ success("Aether SDK added to Packages/manifest.json");
214
+ log("Unity will download the package on next Editor focus.");
215
+ return true;
216
+ } catch (e) {
217
+ warn(`Failed to update manifest.json: ${e.message}`);
218
+ warn(`Manually add the SDK: ${UPM_GIT_URL}`);
219
+ return false;
220
+ }
221
+ }
222
+
223
+ function writeCursorConfig(bridgePath) {
224
+ const cwd = process.cwd();
225
+ const cursorDir = join(cwd, ".cursor");
226
+ const mcpJsonPath = join(cursorDir, "mcp.json");
227
+
228
+ // Normalize path to forward slashes (Cursor expects this on all platforms)
229
+ const normalizedPath = bridgePath.replace(/\\/g, "/");
230
+
231
+ mkdirSync(cursorDir, { recursive: true });
232
+
233
+ let config;
234
+ if (existsSync(mcpJsonPath)) {
235
+ try {
236
+ config = JSON.parse(readFileSync(mcpJsonPath, "utf-8"));
237
+ } catch {
238
+ config = {};
239
+ }
240
+ } else {
241
+ config = {};
242
+ }
243
+
244
+ if (!config.mcpServers) config.mcpServers = {};
245
+ config.mcpServers.aether = {
246
+ command: normalizedPath,
247
+ args: ["--stdio"],
248
+ };
249
+
250
+ writeFileSync(mcpJsonPath, JSON.stringify(config, null, 2) + "\n");
251
+ success(`Cursor MCP configured: ${mcpJsonPath}`);
252
+ }
253
+
254
+ // Vision is handled automatically via Aether's cloud proxy for Pro/Studio users.
255
+ // No local setup needed. Users who want local privacy can set up Ollama manually.
256
+ // See: https://getaether.dev/docs/tools#vision
257
+
258
+ // =============================================================================
259
+ // Main
260
+ // =============================================================================
261
+
262
+ async function main() {
263
+ console.log();
264
+ console.log(" \x1b[1m\x1b[36mAether\x1b[0m - AI co-debugging for Unity");
265
+ console.log(" \x1b[2mStop narrating. Your AI can see the game.\x1b[0m");
266
+ console.log();
267
+
268
+ try {
269
+ // Step 1: Download bridge binary
270
+ log("Step 1/3: Downloading Aether Bridge...");
271
+ const bridgePath = await downloadBridge();
272
+
273
+ // Step 2: Install Unity SDK (if in a Unity project)
274
+ log("Step 2/3: Installing Unity SDK...");
275
+ const unityProject = findUnityProject();
276
+ if (unityProject) {
277
+ log(`Unity project found: ${unityProject}`);
278
+ installUnitySdk(unityProject);
279
+ } else {
280
+ warn("Not in a Unity project directory. Skipping SDK install.");
281
+ warn("Run this from your Unity project root, or install manually:");
282
+ warn(` UPM: ${UPM_GIT_URL}`);
283
+ }
284
+
285
+ // Step 3: Write Cursor MCP config
286
+ log("Step 3/3: Configuring Cursor IDE...");
287
+ writeCursorConfig(bridgePath);
288
+
289
+ // Done!
290
+ console.log();
291
+ success("Aether is ready!");
292
+ console.log();
293
+ console.log(" Next steps:");
294
+ console.log(" \x1b[1m1.\x1b[0m Reload Cursor (Ctrl+Shift+P > \"Reload Window\")");
295
+ console.log(" \x1b[1m2.\x1b[0m Enter Play mode in Unity");
296
+ console.log(" \x1b[1m3.\x1b[0m Ask your AI: \"What's the state of my game?\"");
297
+ console.log();
298
+ console.log(" \x1b[2m14-day Pro trial included. Cloud vision analysis works automatically.\x1b[0m");
299
+ console.log();
300
+ } catch (e) {
301
+ error(`Setup failed: ${e.message}`);
302
+ console.log();
303
+ console.log(" Troubleshooting:");
304
+ console.log(" - Check your internet connection");
305
+ console.log(" - Try manual download: https://getaether.dev/download");
306
+ console.log(" - Run: aether-bridge --doctor");
307
+ console.log();
308
+ process.exit(1);
309
+ }
310
+ }
311
+
312
+ main();
package/package.json ADDED
@@ -0,0 +1,17 @@
1
+ {
2
+ "name": "aether-init",
3
+ "version": "0.4.0",
4
+ "description": "One-command installer for Aether - AI co-debugging for Unity",
5
+ "bin": {
6
+ "aether-init": "./index.mjs"
7
+ },
8
+ "files": ["index.mjs"],
9
+ "type": "module",
10
+ "keywords": ["aether", "unity", "mcp", "debugging", "ai", "cursor"],
11
+ "author": "Finexma",
12
+ "license": "MIT",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "https://github.com/finexma-dev/aether.git"
16
+ }
17
+ }