@cloudless-ai/node-agent 0.0.2 → 0.0.14

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 CHANGED
@@ -11,7 +11,7 @@ npm install -g @cloudless-ai/node-agent
11
11
  During install, the script:
12
12
 
13
13
  1. Detects your OS/CPU architecture.
14
- 2. Installs the matching `cloudless-agent` binary.
14
+ 2. Installs the matching precompiled binary bundled in this package.
15
15
  3. On macOS, also installs `CloudlessMenuBar`.
16
16
  4. Prompts for your router API key and writes `~/.cloudless/agent.yaml`.
17
17
 
@@ -19,10 +19,11 @@ During install, the script:
19
19
 
20
20
  ```bash
21
21
  cloudless-node start
22
+ cloudless-node start --interactive
22
23
  cloudless-node menubar
24
+ cloudless-node stop
23
25
  cloudless-node set-api-key
24
- cloudless-node configure --router http://router-host:8080 --token YOUR_KEY
25
- cloudless-node paths
26
+ cloudless-node configure
26
27
  ```
27
28
 
28
29
  ## Non-interactive install
@@ -35,4 +36,7 @@ CLOUDLESS_NODE_API_KEY=your_key CLOUDLESS_ROUTER_URL=http://router-host:8080 npm
35
36
 
36
37
  - By default binaries are installed to `~/.cloudless/bin`.
37
38
  - Configuration is written to `~/.cloudless/agent.yaml`.
38
- - Set `CLOUDLESS_RELEASE_BASE_URL` to override release download source.
39
+ - `cloudless-node start` launches the node + menubar in the background by default.
40
+ - Use `cloudless-node start --interactive` to keep it attached to your terminal.
41
+ - Installation has no GitHub release dependency; binaries are bundled in the npm tarball.
42
+ - For internet demos, set `router.advertise_address` in `~/.cloudless/agent.yaml` (or `CLOUDLESS_AGENT_ADVERTISE_ADDRESS`) to a public tunnel URL so router/dashboard can reach this node.
@@ -1,121 +1,255 @@
1
1
  #!/usr/bin/env node
2
+ const { spawn, spawnSync } = require("node:child_process");
2
3
  const fs = require("node:fs");
3
- const { spawnSync } = require("node:child_process");
4
4
  const path = require("node:path");
5
5
 
6
6
  const {
7
7
  AGENT_BINARY_NAME,
8
8
  MENUBAR_BINARY_NAME,
9
9
  BIN_DIR,
10
- CONFIG_PATH,
10
+ CLOUDLESS_ROOT,
11
11
  promptSecret,
12
+ promptLine,
12
13
  writeOrUpdateConfig,
14
+ CONFIG_PATH,
13
15
  } = require("../scripts/common");
14
16
 
15
- function usage() {
16
- process.stdout.write(`Cloudless AI Node CLI
17
+ const agentBin = path.join(BIN_DIR, AGENT_BINARY_NAME);
18
+ const menubarBin = path.join(BIN_DIR, MENUBAR_BINARY_NAME);
19
+ const supervisorPidFile = path.join(CLOUDLESS_ROOT, "cloudless-node-supervisor.pid");
17
20
 
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
- `);
21
+ function withDefaultConfig(args) {
22
+ const hasConfigArg = args.includes("--config");
23
+ if (hasConfigArg) {
24
+ return args;
25
+ }
26
+ if (!fs.existsSync(CONFIG_PATH)) {
27
+ return args;
28
+ }
29
+ return [...args, "--config", CONFIG_PATH];
25
30
  }
26
31
 
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
+ function checkBinary(bin) {
33
+ if (!fs.existsSync(bin)) {
34
+ process.stderr.write(
35
+ `cloudless-node: binary not found at ${bin}\n` +
36
+ `Re-run \`npm install -g @cloudless-ai/node-agent\` to reinstall.\n`
37
+ );
32
38
  process.exit(1);
33
39
  }
34
- return fullPath;
35
40
  }
36
41
 
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);
42
+ function runBinary(bin, args) {
43
+ checkBinary(bin);
44
+ const result = spawnSync(bin, withDefaultConfig(args), { stdio: "inherit" });
45
+ if (result.error) {
46
+ process.stderr.write(`cloudless-node: ${result.error.message}\n`);
47
+ process.exit(1);
48
+ }
49
+ process.exit(result.status ?? 0);
43
50
  }
44
51
 
45
- function parseFlag(flag) {
46
- const idx = process.argv.indexOf(flag);
47
- if (idx < 0 || idx + 1 >= process.argv.length) {
48
- return "";
52
+ function stopProcess(pid) {
53
+ try {
54
+ process.kill(pid, "SIGTERM");
55
+ return true;
56
+ } catch {
57
+ return false;
49
58
  }
50
- return process.argv[idx + 1];
51
59
  }
52
60
 
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);
61
+ function readSupervisorPid() {
62
+ if (!fs.existsSync(supervisorPidFile)) {
63
+ return null;
64
+ }
65
+ const raw = fs.readFileSync(supervisorPidFile, "utf8").trim();
66
+ if (!raw) {
67
+ return null;
68
+ }
69
+ const pid = Number(raw);
70
+ if (!Number.isInteger(pid) || pid <= 0) {
71
+ return null;
72
+ }
73
+ try {
74
+ process.kill(pid, 0);
75
+ return pid;
76
+ } catch {
77
+ return null;
58
78
  }
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
79
  }
63
80
 
64
- async function main() {
65
- const command = process.argv[2] ?? "help";
66
- if (command === "help" || command === "--help" || command === "-h") {
67
- usage();
68
- return;
81
+ function writeSupervisorPid() {
82
+ fs.mkdirSync(path.dirname(supervisorPidFile), { recursive: true });
83
+ fs.writeFileSync(supervisorPidFile, `${process.pid}\n`, "utf8");
84
+ }
85
+
86
+ function clearSupervisorPid() {
87
+ try {
88
+ fs.unlinkSync(supervisorPidFile);
89
+ } catch {}
90
+ }
91
+
92
+ function runSupervisor(agentArgs) {
93
+ checkBinary(agentBin);
94
+
95
+ const isMac = process.platform === "darwin";
96
+ const startTime = Date.now();
97
+
98
+ const agent = spawn(agentBin, withDefaultConfig(agentArgs), { stdio: "inherit" });
99
+
100
+ let menubar = null;
101
+ if (isMac) {
102
+ if (fs.existsSync(menubarBin)) {
103
+ menubar = spawn(menubarBin, [], { stdio: "ignore" });
104
+ }
69
105
  }
70
106
 
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;
107
+ writeSupervisorPid();
108
+
109
+ let shuttingDown = false;
110
+ function shutdown() {
111
+ if (shuttingDown) {
112
+ return;
113
+ }
114
+ shuttingDown = true;
115
+ agent.kill();
116
+ if (menubar) menubar.kill();
76
117
  }
77
118
 
78
- if (command === "set-api-key") {
79
- await setAPIKeyFromPrompt();
80
- return;
119
+ process.on("SIGINT", shutdown);
120
+ process.on("SIGTERM", shutdown);
121
+ process.on("exit", clearSupervisorPid);
122
+
123
+ agent.on("exit", (code) => {
124
+ const exitedQuickly = Date.now() - startTime < 5000;
125
+ if (menubar && code !== 0 && code !== null && exitedQuickly) {
126
+ process.stderr.write(
127
+ "cloudless-node: agent failed to start, keeping menubar attached to existing local agent if available\n"
128
+ );
129
+ return;
130
+ }
131
+ if (menubar) menubar.kill();
132
+ clearSupervisorPid();
133
+ process.exit(code ?? 0);
134
+ });
135
+
136
+ if (menubar) {
137
+ menubar.on("exit", () => {
138
+ agent.kill();
139
+ clearSupervisorPid();
140
+ process.exit(0);
141
+ });
142
+ menubar.on("exit", (code) => {
143
+ if (code !== 0 && code !== null) {
144
+ process.stderr.write(`cloudless-node: menubar exited with code ${code}\n`);
145
+ }
146
+ });
81
147
  }
148
+ }
82
149
 
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);
150
+ function parseStartArgs(args) {
151
+ let interactive = false;
152
+ const agentArgs = [];
153
+ for (const arg of args) {
154
+ if (arg === "--interactive") {
155
+ interactive = true;
156
+ continue;
89
157
  }
90
- writeOrUpdateConfig({ routerURL: router, token });
91
- process.stdout.write(`Updated config: ${CONFIG_PATH}\n`);
158
+ agentArgs.push(arg);
159
+ }
160
+ return { interactive, agentArgs };
161
+ }
162
+
163
+ function startDetached(agentArgs) {
164
+ const existing = readSupervisorPid();
165
+ if (existing) {
166
+ process.stdout.write(`cloudless-node: already running (supervisor pid ${existing})\n`);
92
167
  return;
93
168
  }
169
+ const child = spawn(process.execPath, [__filename, "run-supervised", ...agentArgs], {
170
+ detached: true,
171
+ stdio: "ignore",
172
+ });
173
+ child.unref();
174
+ process.stdout.write(`cloudless-node: started in background (supervisor pid ${child.pid})\n`);
175
+ }
94
176
 
95
- if (command === "start") {
96
- const agent = requiredBinaryPath(AGENT_BINARY_NAME);
97
- run(agent, ["--config", CONFIG_PATH]);
177
+ function stopSupervisor() {
178
+ const pid = readSupervisorPid();
179
+ if (!pid) {
180
+ process.stdout.write("cloudless-node: not running\n");
98
181
  return;
99
182
  }
183
+ if (!stopProcess(pid)) {
184
+ clearSupervisorPid();
185
+ process.stdout.write("cloudless-node: not running\n");
186
+ return;
187
+ }
188
+ process.stdout.write("cloudless-node: stop signal sent\n");
189
+ }
190
+
191
+ async function configure() {
192
+ const defaultURL = "http://localhost:8080";
193
+ process.stdout.write("Cloudless AI node configuration\n\n");
100
194
 
101
- if (command === "menubar") {
102
- if (process.platform !== "darwin") {
103
- process.stderr.write("Menu bar app is only available on macOS.\n");
195
+ const routerURL =
196
+ (await promptLine(`Router URL [${defaultURL}]: `)) || defaultURL;
197
+ process.stdout.write(
198
+ "API key: create a node-scoped key in the Cloudless AI dashboard (Admin → API Keys → node scope).\n"
199
+ );
200
+ const apiKey = await promptSecret("Router API key (hidden): ");
201
+
202
+ if (!apiKey) {
203
+ process.stderr.write("API key is required.\n");
204
+ process.exit(1);
205
+ }
206
+
207
+ writeOrUpdateConfig({ routerURL, token: apiKey });
208
+ process.stdout.write(`\nConfig saved to ${CONFIG_PATH}\n`);
209
+ process.stdout.write("Run `cloudless-node start` to launch the node agent.\n");
210
+ }
211
+
212
+ const [, , cmd, ...rest] = process.argv;
213
+
214
+ switch (cmd) {
215
+ case "configure":
216
+ case "set-api-key":
217
+ configure().catch((err) => {
218
+ process.stderr.write(`${err.message}\n`);
104
219
  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
220
  });
111
- return;
221
+ break;
222
+
223
+ case "menubar":
224
+ case "start": {
225
+ const { interactive, agentArgs } = parseStartArgs(rest);
226
+ if (interactive) {
227
+ runSupervisor(agentArgs);
228
+ break;
229
+ }
230
+ startDetached(agentArgs);
231
+ break;
112
232
  }
113
233
 
114
- usage();
115
- process.exit(1);
116
- }
234
+ case "run-supervised":
235
+ runSupervisor(rest);
236
+ break;
117
237
 
118
- main().catch((error) => {
119
- process.stderr.write(`${error.message}\n`);
120
- process.exit(1);
121
- });
238
+ case "stop":
239
+ stopSupervisor();
240
+ break;
241
+
242
+ case undefined: {
243
+ const { interactive, agentArgs } = parseStartArgs(rest);
244
+ if (interactive) {
245
+ runSupervisor(agentArgs);
246
+ break;
247
+ }
248
+ startDetached(agentArgs);
249
+ break;
250
+ }
251
+
252
+ default:
253
+ // Pass unknown subcommands/flags straight to the agent binary
254
+ runBinary(agentBin, [cmd, ...rest]);
255
+ }
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@cloudless-ai/node-agent",
3
- "version": "0.0.2",
3
+ "version": "0.0.14",
4
4
  "description": "Cloudless AI node agent installer and launcher",
5
5
  "license": "MIT",
6
+ "homepage": "https://cloudless-ai.app",
6
7
  "repository": {
7
8
  "type": "git",
8
- "url": "https://github.com/cloudless-ai/cloudless-ai.git",
9
- "directory": "apps/node-agent/npm"
9
+ "url": "https://github.com/cloudless-ai/node-agent"
10
10
  },
11
11
  "bin": {
12
- "cloudless-node": "./bin/cloudless-node.js"
12
+ "cloudless-node": "bin/cloudless-node.js"
13
13
  },
14
14
  "scripts": {
15
15
  "postinstall": "node ./scripts/postinstall.js"
@@ -19,6 +19,7 @@
19
19
  },
20
20
  "files": [
21
21
  "bin",
22
+ "platform",
22
23
  "scripts",
23
24
  "README.md"
24
25
  ],
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
package/scripts/common.js CHANGED
@@ -1,8 +1,6 @@
1
1
  const fs = require("node:fs");
2
2
  const os = require("node:os");
3
3
  const path = require("node:path");
4
- const https = require("node:https");
5
- const { execFileSync } = require("node:child_process");
6
4
  const readline = require("node:readline");
7
5
 
8
6
  const HOME = os.homedir();
@@ -30,82 +28,11 @@ function detectTarget() {
30
28
  return { goos, goarch };
31
29
  }
32
30
 
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
31
  function ensureDirs() {
49
32
  fs.mkdirSync(BIN_DIR, { recursive: true });
50
33
  fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
51
34
  }
52
35
 
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
36
  function installBinary(sourcePath, binaryName) {
110
37
  const targetPath = path.join(BIN_DIR, binaryName);
111
38
  fs.copyFileSync(sourcePath, targetPath);
@@ -185,6 +112,7 @@ function defaultAgentConfig(routerURL, token) {
185
112
  discover_interval: "1m"
186
113
  servers_file: "${HOME}/.cloudless/router-servers.json"
187
114
  manual_servers: []
115
+ # advertise_address: "https://my-node-demo.example.com"
188
116
 
189
117
  inference:
190
118
  backend: "llamacpp"
@@ -272,14 +200,9 @@ module.exports = {
272
200
  defaultAgentConfig,
273
201
  detectTarget,
274
202
  ensureDirs,
275
- extractArchive,
276
- fetchToFile,
277
- findFileRecursive,
278
203
  installBinary,
279
- normalizeVersion,
280
204
  promptLine,
281
205
  promptSecret,
282
- releaseBaseURL,
283
206
  updateRouterConfig,
284
207
  writeOrUpdateConfig,
285
208
  };
@@ -1,128 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
  const fs = require("node:fs");
3
3
  const path = require("node:path");
4
- const os = require("node:os");
5
- const { execFileSync } = require("node:child_process");
6
4
 
7
5
  const {
8
6
  AGENT_BINARY_NAME,
9
7
  MENUBAR_BINARY_NAME,
10
- BIN_DIR,
11
8
  CONFIG_PATH,
12
9
  detectTarget,
13
10
  ensureDirs,
14
- extractArchive,
15
- fetchToFile,
16
- findFileRecursive,
17
11
  installBinary,
18
- normalizeVersion,
19
12
  promptSecret,
20
- releaseBaseURL,
21
13
  writeOrUpdateConfig,
22
14
  } = require("./common");
23
15
 
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");
16
+ function resolveBundledPlatformDir(target) {
17
+ const key = `${target.goos}/${target.goarch}`;
18
+ const dirName = `${target.goos}-${target.goarch}`;
19
+ const platformDir = path.resolve(__dirname, "..", "platform", dirName);
20
+ if (!fs.existsSync(platformDir)) {
21
+ throw new Error(`Unsupported platform/arch: ${key}`);
52
22
  }
53
- downloadedSourceRoot = path.join(extractDir, entries[0].name);
54
- return downloadedSourceRoot;
23
+ return platformDir;
55
24
  }
56
25
 
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
- }
26
+ function installFromPlatformPackage(target) {
27
+ const platformDir = resolveBundledPlatformDir(target);
28
+ const agentSource = path.join(platformDir, AGENT_BINARY_NAME);
89
29
 
90
- async function installMenubar(version) {
91
- if (process.platform !== "darwin") {
92
- return null;
30
+ if (!fs.existsSync(agentSource)) {
31
+ throw new Error(`Bundled binaries are missing ${AGENT_BINARY_NAME} for ${target.goos}/${target.goarch}`);
93
32
  }
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");
33
+ const agentPath = installBinary(agentSource, AGENT_BINARY_NAME);
99
34
 
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}`);
35
+ let menubarPath = null;
36
+ if (target.goos === "darwin") {
37
+ const menuBarSource = path.join(platformDir, MENUBAR_BINARY_NAME);
38
+ if (!fs.existsSync(menuBarSource)) {
39
+ throw new Error(`Bundled binaries are missing ${MENUBAR_BINARY_NAME} for ${target.goos}/${target.goarch}`);
106
40
  }
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 });
41
+ menubarPath = installBinary(menuBarSource, MENUBAR_BINARY_NAME);
125
42
  }
43
+
44
+ return { agentPath, menubarPath };
126
45
  }
127
46
 
128
47
  async function promptConfig() {
@@ -135,7 +54,7 @@ async function promptConfig() {
135
54
  }
136
55
  if (!apiKey) {
137
56
  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");
57
+ process.stdout.write("Run `cloudless-node configure` to set up your router URL and API key.\n");
139
58
  return;
140
59
  }
141
60
  writeOrUpdateConfig({ routerURL, token: apiKey });
@@ -143,30 +62,18 @@ async function promptConfig() {
143
62
  }
144
63
 
145
64
  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
- }
65
+ ensureDirs();
66
+ const target = detectTarget();
67
+ process.stdout.write(`Installing Cloudless node agent for ${target.goos}/${target.goarch}...\n`);
68
+ const result = installFromPlatformPackage(target);
69
+ process.stdout.write(`Installed ${result.agentPath}\n`);
70
+ if (result.menubarPath) {
71
+ process.stdout.write(`Installed ${result.menubarPath}\n`);
72
+ }
73
+ await promptConfig();
74
+ process.stdout.write("Done. Run `cloudless-node start` to launch the node agent.\n");
75
+ if (target.goos === "darwin") {
76
+ process.stdout.write("Run `cloudless-node menubar` to start the menu bar app.\n");
170
77
  }
171
78
  }
172
79