@cloudless-ai/node-agent 0.0.2

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 ADDED
@@ -0,0 +1,38 @@
1
+ # @cloudless-ai/node-agent
2
+
3
+ Install and manage the Cloudless AI node agent from npm.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g @cloudless-ai/node-agent
9
+ ```
10
+
11
+ During install, the script:
12
+
13
+ 1. Detects your OS/CPU architecture.
14
+ 2. Installs the matching `cloudless-agent` binary.
15
+ 3. On macOS, also installs `CloudlessMenuBar`.
16
+ 4. Prompts for your router API key and writes `~/.cloudless/agent.yaml`.
17
+
18
+ ## Commands
19
+
20
+ ```bash
21
+ cloudless-node start
22
+ cloudless-node menubar
23
+ cloudless-node set-api-key
24
+ cloudless-node configure --router http://router-host:8080 --token YOUR_KEY
25
+ cloudless-node paths
26
+ ```
27
+
28
+ ## Non-interactive install
29
+
30
+ ```bash
31
+ CLOUDLESS_NODE_API_KEY=your_key CLOUDLESS_ROUTER_URL=http://router-host:8080 npm install -g @cloudless-ai/node-agent
32
+ ```
33
+
34
+ ## Notes
35
+
36
+ - By default binaries are installed to `~/.cloudless/bin`.
37
+ - Configuration is written to `~/.cloudless/agent.yaml`.
38
+ - Set `CLOUDLESS_RELEASE_BASE_URL` to override release download source.
@@ -0,0 +1,121 @@
1
+ #!/usr/bin/env node
2
+ const fs = require("node:fs");
3
+ const { spawnSync } = require("node:child_process");
4
+ const path = require("node:path");
5
+
6
+ const {
7
+ AGENT_BINARY_NAME,
8
+ MENUBAR_BINARY_NAME,
9
+ BIN_DIR,
10
+ CONFIG_PATH,
11
+ promptSecret,
12
+ writeOrUpdateConfig,
13
+ } = require("../scripts/common");
14
+
15
+ function usage() {
16
+ process.stdout.write(`Cloudless AI Node CLI
17
+
18
+ Usage:
19
+ cloudless-node start
20
+ cloudless-node menubar
21
+ cloudless-node set-api-key
22
+ cloudless-node configure --router <url> --token <api-key>
23
+ cloudless-node paths
24
+ `);
25
+ }
26
+
27
+ function requiredBinaryPath(name) {
28
+ const fullPath = path.join(BIN_DIR, name);
29
+ if (!fs.existsSync(fullPath)) {
30
+ process.stderr.write(`Binary not found: ${fullPath}\n`);
31
+ process.stderr.write("Reinstall package: npm install -g @cloudless-ai/node-agent\n");
32
+ process.exit(1);
33
+ }
34
+ return fullPath;
35
+ }
36
+
37
+ function run(command, args, extraEnv = {}) {
38
+ const result = spawnSync(command, args, {
39
+ stdio: "inherit",
40
+ env: { ...process.env, ...extraEnv },
41
+ });
42
+ process.exit(result.status ?? 1);
43
+ }
44
+
45
+ function parseFlag(flag) {
46
+ const idx = process.argv.indexOf(flag);
47
+ if (idx < 0 || idx + 1 >= process.argv.length) {
48
+ return "";
49
+ }
50
+ return process.argv[idx + 1];
51
+ }
52
+
53
+ async function setAPIKeyFromPrompt() {
54
+ const token = await promptSecret("Enter Cloudless router API key (hidden): ");
55
+ if (!token) {
56
+ process.stderr.write("No API key provided.\n");
57
+ process.exit(1);
58
+ }
59
+ const routerURL = process.env.CLOUDLESS_ROUTER_URL?.trim() || "http://localhost:8080";
60
+ writeOrUpdateConfig({ routerURL, token });
61
+ process.stdout.write(`Updated API key in ${CONFIG_PATH}\n`);
62
+ }
63
+
64
+ async function main() {
65
+ const command = process.argv[2] ?? "help";
66
+ if (command === "help" || command === "--help" || command === "-h") {
67
+ usage();
68
+ return;
69
+ }
70
+
71
+ if (command === "paths") {
72
+ process.stdout.write(`Agent binary: ${path.join(BIN_DIR, AGENT_BINARY_NAME)}\n`);
73
+ process.stdout.write(`Menu bar binary: ${path.join(BIN_DIR, MENUBAR_BINARY_NAME)}\n`);
74
+ process.stdout.write(`Config: ${CONFIG_PATH}\n`);
75
+ return;
76
+ }
77
+
78
+ if (command === "set-api-key") {
79
+ await setAPIKeyFromPrompt();
80
+ return;
81
+ }
82
+
83
+ if (command === "configure") {
84
+ const router = parseFlag("--router") || process.env.CLOUDLESS_ROUTER_URL || "http://localhost:8080";
85
+ const token = parseFlag("--token") || process.env.CLOUDLESS_NODE_API_KEY || "";
86
+ if (!token) {
87
+ process.stderr.write("Missing API key. Use --token or CLOUDLESS_NODE_API_KEY.\n");
88
+ process.exit(1);
89
+ }
90
+ writeOrUpdateConfig({ routerURL: router, token });
91
+ process.stdout.write(`Updated config: ${CONFIG_PATH}\n`);
92
+ return;
93
+ }
94
+
95
+ if (command === "start") {
96
+ const agent = requiredBinaryPath(AGENT_BINARY_NAME);
97
+ run(agent, ["--config", CONFIG_PATH]);
98
+ return;
99
+ }
100
+
101
+ if (command === "menubar") {
102
+ if (process.platform !== "darwin") {
103
+ process.stderr.write("Menu bar app is only available on macOS.\n");
104
+ process.exit(1);
105
+ }
106
+ const menubar = requiredBinaryPath(MENUBAR_BINARY_NAME);
107
+ run(menubar, [], {
108
+ CLOUDLESS_AGENT_CONFIG_PATH: CONFIG_PATH,
109
+ CLOUDLESS_AGENT_STATUS_URL: process.env.CLOUDLESS_AGENT_STATUS_URL || "http://127.0.0.1:9090/v1/local/status",
110
+ });
111
+ return;
112
+ }
113
+
114
+ usage();
115
+ process.exit(1);
116
+ }
117
+
118
+ main().catch((error) => {
119
+ process.stderr.write(`${error.message}\n`);
120
+ process.exit(1);
121
+ });
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "@cloudless-ai/node-agent",
3
+ "version": "0.0.2",
4
+ "description": "Cloudless AI node agent installer and launcher",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/cloudless-ai/cloudless-ai.git",
9
+ "directory": "apps/node-agent/npm"
10
+ },
11
+ "bin": {
12
+ "cloudless-node": "./bin/cloudless-node.js"
13
+ },
14
+ "scripts": {
15
+ "postinstall": "node ./scripts/postinstall.js"
16
+ },
17
+ "engines": {
18
+ "node": ">=18"
19
+ },
20
+ "files": [
21
+ "bin",
22
+ "scripts",
23
+ "README.md"
24
+ ],
25
+ "keywords": [
26
+ "cloudless-ai",
27
+ "node-agent",
28
+ "llm",
29
+ "inference"
30
+ ]
31
+ }
@@ -0,0 +1,285 @@
1
+ const fs = require("node:fs");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+ const https = require("node:https");
5
+ const { execFileSync } = require("node:child_process");
6
+ const readline = require("node:readline");
7
+
8
+ const HOME = os.homedir();
9
+ const CLOUDLESS_ROOT = path.join(HOME, ".cloudless");
10
+ const BIN_DIR = path.join(CLOUDLESS_ROOT, "bin");
11
+ const CONFIG_PATH = path.join(CLOUDLESS_ROOT, "agent.yaml");
12
+ const AGENT_BINARY_NAME = process.platform === "win32" ? "cloudless-agent.exe" : "cloudless-agent";
13
+ const MENUBAR_BINARY_NAME = process.platform === "win32" ? "CloudlessMenuBar.exe" : "CloudlessMenuBar";
14
+
15
+ function detectTarget() {
16
+ const archMap = {
17
+ x64: "amd64",
18
+ arm64: "arm64",
19
+ };
20
+ const osMap = {
21
+ darwin: "darwin",
22
+ linux: "linux",
23
+ win32: "windows",
24
+ };
25
+ const goarch = archMap[process.arch];
26
+ const goos = osMap[process.platform];
27
+ if (!goarch || !goos) {
28
+ throw new Error(`Unsupported platform/arch: ${process.platform}/${process.arch}`);
29
+ }
30
+ return { goos, goarch };
31
+ }
32
+
33
+ function normalizeVersion(rawVersion) {
34
+ if (!rawVersion) {
35
+ return "dev";
36
+ }
37
+ return rawVersion.startsWith("v") ? rawVersion.slice(1) : rawVersion;
38
+ }
39
+
40
+ function releaseBaseURL(version) {
41
+ const override = process.env.CLOUDLESS_RELEASE_BASE_URL;
42
+ if (override && override.trim()) {
43
+ return override.trim().replace(/\/+$/, "");
44
+ }
45
+ return `https://github.com/cloudless-ai/cloudless-ai/releases/download/v${version}`;
46
+ }
47
+
48
+ function ensureDirs() {
49
+ fs.mkdirSync(BIN_DIR, { recursive: true });
50
+ fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
51
+ }
52
+
53
+ function fetchToFile(url, destPath, redirects = 5) {
54
+ return new Promise((resolve, reject) => {
55
+ const req = https.get(url, (res) => {
56
+ const location = res.headers.location;
57
+ if (location && res.statusCode && res.statusCode >= 300 && res.statusCode < 400) {
58
+ res.resume();
59
+ if (redirects <= 0) {
60
+ reject(new Error(`Too many redirects for ${url}`));
61
+ return;
62
+ }
63
+ fetchToFile(location, destPath, redirects - 1).then(resolve).catch(reject);
64
+ return;
65
+ }
66
+ if (res.statusCode !== 200) {
67
+ res.resume();
68
+ reject(new Error(`Download failed (${res.statusCode}) for ${url}`));
69
+ return;
70
+ }
71
+ const out = fs.createWriteStream(destPath);
72
+ res.pipe(out);
73
+ out.on("finish", () => {
74
+ out.close();
75
+ resolve();
76
+ });
77
+ out.on("error", reject);
78
+ });
79
+ req.on("error", reject);
80
+ });
81
+ }
82
+
83
+ function extractArchive(archivePath, destination) {
84
+ fs.mkdirSync(destination, { recursive: true });
85
+ if (archivePath.endsWith(".zip")) {
86
+ execFileSync("unzip", ["-o", archivePath, "-d", destination], { stdio: "ignore" });
87
+ return;
88
+ }
89
+ execFileSync("tar", ["-xzf", archivePath, "-C", destination], { stdio: "ignore" });
90
+ }
91
+
92
+ function findFileRecursive(rootDir, fileName) {
93
+ const entries = fs.readdirSync(rootDir, { withFileTypes: true });
94
+ for (const entry of entries) {
95
+ const fullPath = path.join(rootDir, entry.name);
96
+ if (entry.isFile() && entry.name === fileName) {
97
+ return fullPath;
98
+ }
99
+ if (entry.isDirectory()) {
100
+ const found = findFileRecursive(fullPath, fileName);
101
+ if (found) {
102
+ return found;
103
+ }
104
+ }
105
+ }
106
+ return null;
107
+ }
108
+
109
+ function installBinary(sourcePath, binaryName) {
110
+ const targetPath = path.join(BIN_DIR, binaryName);
111
+ fs.copyFileSync(sourcePath, targetPath);
112
+ fs.chmodSync(targetPath, 0o755);
113
+ return targetPath;
114
+ }
115
+
116
+ function updateRouterConfig(content, { routerURL, token }) {
117
+ const lines = content.split(/\r?\n/);
118
+ let routerLine = -1;
119
+ for (let i = 0; i < lines.length; i++) {
120
+ if (/^\s*router:\s*$/.test(lines[i])) {
121
+ routerLine = i;
122
+ break;
123
+ }
124
+ }
125
+
126
+ if (routerLine === -1) {
127
+ return `${content.trimEnd()}\n\nrouter:\n address: "${routerURL}"\n token: "${token}"\n`;
128
+ }
129
+
130
+ let blockEnd = lines.length;
131
+ for (let i = routerLine + 1; i < lines.length; i++) {
132
+ if (/^\S/.test(lines[i])) {
133
+ blockEnd = i;
134
+ break;
135
+ }
136
+ }
137
+
138
+ let addressLine = -1;
139
+ let tokenLine = -1;
140
+ for (let i = routerLine + 1; i < blockEnd; i++) {
141
+ if (/^\s+address:\s*/.test(lines[i])) {
142
+ addressLine = i;
143
+ }
144
+ if (/^\s+token:\s*/.test(lines[i])) {
145
+ tokenLine = i;
146
+ }
147
+ }
148
+
149
+ if (addressLine >= 0) {
150
+ lines[addressLine] = ` address: "${routerURL}"`;
151
+ } else {
152
+ lines.splice(routerLine + 1, 0, ` address: "${routerURL}"`);
153
+ blockEnd += 1;
154
+ if (tokenLine >= 0) {
155
+ tokenLine += 1;
156
+ }
157
+ }
158
+
159
+ if (tokenLine >= 0) {
160
+ lines[tokenLine] = ` token: "${token}"`;
161
+ } else {
162
+ lines.splice(routerLine + 1, 0, ` token: "${token}"`);
163
+ }
164
+
165
+ return `${lines.join("\n").replace(/\s+$/, "")}\n`;
166
+ }
167
+
168
+ function writeOrUpdateConfig({ routerURL, token }) {
169
+ let content = defaultAgentConfig(routerURL, token);
170
+ if (fs.existsSync(CONFIG_PATH)) {
171
+ const existing = fs.readFileSync(CONFIG_PATH, "utf8");
172
+ content = updateRouterConfig(existing, { routerURL, token });
173
+ }
174
+ fs.writeFileSync(CONFIG_PATH, content, "utf8");
175
+ }
176
+
177
+ function defaultAgentConfig(routerURL, token) {
178
+ return `router:
179
+ address: "${routerURL}"
180
+ token: "${token}"
181
+ heartbeat_interval: "10s"
182
+ system_load_ceiling: 2.5
183
+ mdns: true
184
+ mdns_service: "_cloudless-router._tcp"
185
+ discover_interval: "1m"
186
+ servers_file: "${HOME}/.cloudless/router-servers.json"
187
+ manual_servers: []
188
+
189
+ inference:
190
+ backend: "llamacpp"
191
+ llamacpp_host: "http://localhost:8081"
192
+ models_dir: "${HOME}/.cloudless/models"
193
+ preload_models: []
194
+ max_concurrent_requests: 4
195
+ auto_install_llamacpp: false
196
+ unload_on_system_load: true
197
+
198
+ server:
199
+ port: 9090
200
+
201
+ observability:
202
+ log_level: "info"
203
+ tracing:
204
+ enabled: false
205
+ endpoint: "localhost:4318"
206
+ insecure: true
207
+ service_name: "cloudless-node-agent"
208
+ sampling_ratio: 1.0
209
+ `;
210
+ }
211
+
212
+ function promptSecret(promptText) {
213
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
214
+ return Promise.resolve("");
215
+ }
216
+
217
+ return new Promise((resolve) => {
218
+ process.stdout.write(promptText);
219
+ const stdin = process.stdin;
220
+ stdin.setRawMode(true);
221
+ stdin.resume();
222
+ stdin.setEncoding("utf8");
223
+ let value = "";
224
+
225
+ const onData = (char) => {
226
+ if (char === "\r" || char === "\n") {
227
+ process.stdout.write("\n");
228
+ stdin.setRawMode(false);
229
+ stdin.pause();
230
+ stdin.removeListener("data", onData);
231
+ resolve(value.trim());
232
+ return;
233
+ }
234
+ if (char === "\u0003") {
235
+ process.stdout.write("\n");
236
+ process.exit(130);
237
+ }
238
+ if (char === "\u007f") {
239
+ if (value.length > 0) {
240
+ value = value.slice(0, -1);
241
+ process.stdout.write("\b \b");
242
+ }
243
+ return;
244
+ }
245
+ value += char;
246
+ process.stdout.write("*");
247
+ };
248
+
249
+ stdin.on("data", onData);
250
+ });
251
+ }
252
+
253
+ function promptLine(promptText) {
254
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
255
+ return Promise.resolve("");
256
+ }
257
+ return new Promise((resolve) => {
258
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
259
+ rl.question(promptText, (answer) => {
260
+ rl.close();
261
+ resolve(answer.trim());
262
+ });
263
+ });
264
+ }
265
+
266
+ module.exports = {
267
+ AGENT_BINARY_NAME,
268
+ MENUBAR_BINARY_NAME,
269
+ BIN_DIR,
270
+ CONFIG_PATH,
271
+ CLOUDLESS_ROOT,
272
+ defaultAgentConfig,
273
+ detectTarget,
274
+ ensureDirs,
275
+ extractArchive,
276
+ fetchToFile,
277
+ findFileRecursive,
278
+ installBinary,
279
+ normalizeVersion,
280
+ promptLine,
281
+ promptSecret,
282
+ releaseBaseURL,
283
+ updateRouterConfig,
284
+ writeOrUpdateConfig,
285
+ };
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ const fs = require("node:fs");
3
+ const path = require("node:path");
4
+ const os = require("node:os");
5
+ const { execFileSync } = require("node:child_process");
6
+
7
+ const {
8
+ AGENT_BINARY_NAME,
9
+ MENUBAR_BINARY_NAME,
10
+ BIN_DIR,
11
+ CONFIG_PATH,
12
+ detectTarget,
13
+ ensureDirs,
14
+ extractArchive,
15
+ fetchToFile,
16
+ findFileRecursive,
17
+ installBinary,
18
+ normalizeVersion,
19
+ promptSecret,
20
+ releaseBaseURL,
21
+ writeOrUpdateConfig,
22
+ } = require("./common");
23
+
24
+ const pkg = require("../package.json");
25
+ let downloadedSourceRoot = null;
26
+ let downloadedSourceTempDir = null;
27
+
28
+ async function ensureSourceTree(version) {
29
+ if (downloadedSourceRoot) {
30
+ return downloadedSourceRoot;
31
+ }
32
+ const localRoot = path.resolve(__dirname, "..", "..");
33
+ if (fs.existsSync(path.join(localRoot, "go.mod"))) {
34
+ downloadedSourceRoot = path.resolve(localRoot, "..", "..");
35
+ return downloadedSourceRoot;
36
+ }
37
+ if (version === "dev") {
38
+ return null;
39
+ }
40
+
41
+ const tarballURL = `https://codeload.github.com/cloudless-ai/cloudless-ai/tar.gz/refs/tags/v${version}`;
42
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cloudless-source-"));
43
+ downloadedSourceTempDir = tempDir;
44
+ const archivePath = path.join(tempDir, `cloudless-ai-v${version}.tar.gz`);
45
+ const extractDir = path.join(tempDir, "extract");
46
+
47
+ await fetchToFile(tarballURL, archivePath);
48
+ extractArchive(archivePath, extractDir);
49
+ const entries = fs.readdirSync(extractDir, { withFileTypes: true }).filter((entry) => entry.isDirectory());
50
+ if (entries.length !== 1) {
51
+ throw new Error("Unable to locate extracted source directory");
52
+ }
53
+ downloadedSourceRoot = path.join(extractDir, entries[0].name);
54
+ return downloadedSourceRoot;
55
+ }
56
+
57
+ async function installAgent(version, target) {
58
+ const ext = target.goos === "windows" ? ".zip" : ".tar.gz";
59
+ const archiveName = `cloudless-agent_${version}_${target.goos}_${target.goarch}${ext}`;
60
+ const url = `${releaseBaseURL(version)}/${archiveName}`;
61
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cloudless-agent-"));
62
+ const archivePath = path.join(tempDir, archiveName);
63
+ const extractDir = path.join(tempDir, "extract");
64
+
65
+ try {
66
+ await fetchToFile(url, archivePath);
67
+ extractArchive(archivePath, extractDir);
68
+ const binary = findFileRecursive(extractDir, AGENT_BINARY_NAME);
69
+ if (!binary) {
70
+ throw new Error(`Release archive does not contain ${AGENT_BINARY_NAME}`);
71
+ }
72
+ return installBinary(binary, AGENT_BINARY_NAME);
73
+ } catch (error) {
74
+ const sourceTree = await ensureSourceTree(version);
75
+ const sourceRoot = sourceTree ? path.join(sourceTree, "apps", "node-agent") : null;
76
+ if (!sourceRoot || !fs.existsSync(path.join(sourceRoot, "go.mod"))) {
77
+ throw error;
78
+ }
79
+ execFileSync("go", ["build", "-o", path.join(BIN_DIR, AGENT_BINARY_NAME), "./cmd/agent"], {
80
+ cwd: sourceRoot,
81
+ stdio: "inherit",
82
+ });
83
+ fs.chmodSync(path.join(BIN_DIR, AGENT_BINARY_NAME), 0o755);
84
+ return path.join(BIN_DIR, AGENT_BINARY_NAME);
85
+ } finally {
86
+ fs.rmSync(tempDir, { recursive: true, force: true });
87
+ }
88
+ }
89
+
90
+ async function installMenubar(version) {
91
+ if (process.platform !== "darwin") {
92
+ return null;
93
+ }
94
+ const archiveName = `CloudlessMenuBar_${version}_darwin_universal.tar.gz`;
95
+ const url = `${releaseBaseURL(version)}/${archiveName}`;
96
+ const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "cloudless-menubar-"));
97
+ const archivePath = path.join(tempDir, archiveName);
98
+ const extractDir = path.join(tempDir, "extract");
99
+
100
+ try {
101
+ await fetchToFile(url, archivePath);
102
+ extractArchive(archivePath, extractDir);
103
+ const binary = findFileRecursive(extractDir, MENUBAR_BINARY_NAME);
104
+ if (!binary) {
105
+ throw new Error(`Release archive does not contain ${MENUBAR_BINARY_NAME}`);
106
+ }
107
+ return installBinary(binary, MENUBAR_BINARY_NAME);
108
+ } catch {
109
+ const sourceTree = await ensureSourceTree(version);
110
+ const menubarRoot = sourceTree ? path.join(sourceTree, "apps", "menubar-macos") : null;
111
+ if (!menubarRoot || !fs.existsSync(path.join(menubarRoot, "Package.swift"))) {
112
+ return null;
113
+ }
114
+ execFileSync("swift", ["build", "-c", "release", "--product", "CloudlessMenuBar"], {
115
+ cwd: menubarRoot,
116
+ stdio: "inherit",
117
+ });
118
+ const builtBinary = path.join(menubarRoot, ".build", "release", MENUBAR_BINARY_NAME);
119
+ if (!fs.existsSync(builtBinary)) {
120
+ return null;
121
+ }
122
+ return installBinary(builtBinary, MENUBAR_BINARY_NAME);
123
+ } finally {
124
+ fs.rmSync(tempDir, { recursive: true, force: true });
125
+ }
126
+ }
127
+
128
+ async function promptConfig() {
129
+ const routerURL = process.env.CLOUDLESS_ROUTER_URL?.trim() || "http://localhost:8080";
130
+ let apiKey = process.env.CLOUDLESS_NODE_API_KEY?.trim() || "";
131
+ if (!apiKey && process.stdin.isTTY && process.stdout.isTTY) {
132
+ process.stdout.write("\nCloudless AI node setup\n");
133
+ process.stdout.write(`Router URL: ${routerURL}\n`);
134
+ apiKey = await promptSecret("Enter Cloudless router API key (hidden): ");
135
+ }
136
+ if (!apiKey) {
137
+ process.stdout.write("\nNo API key provided during install.\n");
138
+ process.stdout.write("Run `cloudless-node set-api-key` to configure it later.\n");
139
+ return;
140
+ }
141
+ writeOrUpdateConfig({ routerURL, token: apiKey });
142
+ process.stdout.write(`Saved node config to ${CONFIG_PATH}\n`);
143
+ }
144
+
145
+ async function main() {
146
+ try {
147
+ ensureDirs();
148
+ const version = normalizeVersion(pkg.version);
149
+ const target = detectTarget();
150
+
151
+ process.stdout.write(`Installing Cloudless node agent for ${target.goos}/${target.goarch}...\n`);
152
+ const agentPath = await installAgent(version, target);
153
+ process.stdout.write(`Installed ${agentPath}\n`);
154
+
155
+ const menubarPath = await installMenubar(version);
156
+ if (menubarPath) {
157
+ process.stdout.write(`Installed ${menubarPath}\n`);
158
+ }
159
+
160
+ await promptConfig();
161
+ process.stdout.write("Done. Run `cloudless-node start` to launch the node agent.\n");
162
+ if (process.platform === "darwin") {
163
+ process.stdout.write("Run `cloudless-node menubar` to start the menu bar app.\n");
164
+ }
165
+ } finally {
166
+ if (downloadedSourceTempDir) {
167
+ fs.rmSync(downloadedSourceTempDir, { recursive: true, force: true });
168
+ downloadedSourceTempDir = null;
169
+ }
170
+ }
171
+ }
172
+
173
+ main().catch((error) => {
174
+ process.stderr.write(`Installation failed: ${error.message}\n`);
175
+ process.exit(1);
176
+ });