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.
- package/index.mjs +312 -0
- 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
|
+
}
|