@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 +8 -4
- package/bin/cloudless-node.js +210 -76
- package/package.json +5 -4
- package/platform/darwin-amd64/.gitkeep +0 -0
- package/platform/darwin-amd64/CloudlessMenuBar +0 -0
- package/platform/darwin-amd64/cloudless-agent +0 -0
- package/platform/darwin-arm64/.gitkeep +0 -0
- package/platform/darwin-arm64/CloudlessMenuBar +0 -0
- package/platform/darwin-arm64/cloudless-agent +0 -0
- package/platform/linux-amd64/.gitkeep +0 -0
- package/platform/linux-amd64/cloudless-agent +0 -0
- package/platform/linux-arm64/.gitkeep +0 -0
- package/platform/linux-arm64/cloudless-agent +0 -0
- package/platform/windows-amd64/.gitkeep +0 -0
- package/platform/windows-amd64/cloudless-agent.exe +0 -0
- package/platform/windows-arm64/.gitkeep +0 -0
- package/platform/windows-arm64/cloudless-agent.exe +0 -0
- package/scripts/common.js +1 -78
- package/scripts/postinstall.js +34 -127
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
|
|
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
|
|
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
|
-
-
|
|
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.
|
package/bin/cloudless-node.js
CHANGED
|
@@ -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
|
-
|
|
10
|
+
CLOUDLESS_ROOT,
|
|
11
11
|
promptSecret,
|
|
12
|
+
promptLine,
|
|
12
13
|
writeOrUpdateConfig,
|
|
14
|
+
CONFIG_PATH,
|
|
13
15
|
} = require("../scripts/common");
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
91
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
234
|
+
case "run-supervised":
|
|
235
|
+
runSupervisor(rest);
|
|
236
|
+
break;
|
|
117
237
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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.
|
|
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/
|
|
9
|
-
"directory": "apps/node-agent/npm"
|
|
9
|
+
"url": "https://github.com/cloudless-ai/node-agent"
|
|
10
10
|
},
|
|
11
11
|
"bin": {
|
|
12
|
-
"cloudless-node": "
|
|
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
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
|
File without changes
|
|
Binary file
|
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
|
};
|
package/scripts/postinstall.js
CHANGED
|
@@ -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
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
-
|
|
54
|
-
return downloadedSourceRoot;
|
|
23
|
+
return platformDir;
|
|
55
24
|
}
|
|
56
25
|
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
const
|
|
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
|
-
|
|
91
|
-
|
|
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
|
|
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
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|