@openserv-labs/deploy 0.1.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 (3) hide show
  1. package/README.md +125 -0
  2. package/dist/index.js +392 -0
  3. package/package.json +39 -0
package/README.md ADDED
@@ -0,0 +1,125 @@
1
+ # serv
2
+
3
+ CLI to deploy agents to the [OpenServ](https://openserv.ai) platform.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g serv
9
+ ```
10
+
11
+ Or run directly with `npx`:
12
+
13
+ ```bash
14
+ npx serv deploy
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```
20
+ serv <command> [path]
21
+
22
+ Commands:
23
+ deploy [path] Deploy an agent to OpenServ (default path: .)
24
+
25
+ Options:
26
+ --help, -h Show this help message
27
+ ```
28
+
29
+ ### Deploy an agent
30
+
31
+ From the agent project directory:
32
+
33
+ ```bash
34
+ serv deploy
35
+ ```
36
+
37
+ Or specify a path:
38
+
39
+ ```bash
40
+ serv deploy ./my-agent
41
+ ```
42
+
43
+ The deploy command will:
44
+
45
+ 1. Read configuration from `.env` and `.openserv.json` in the target directory.
46
+ 2. Resolve or create a container on the OpenServ platform.
47
+ 3. Archive the project files (respecting `.gitignore`), upload them, and run `npm install`.
48
+ 4. Start (or restart) the agent process and make it publicly available.
49
+ 5. If an agent API key is configured, update the agent's endpoint URL on the platform.
50
+
51
+ ## Configuration
52
+
53
+ ### Environment variables
54
+
55
+ Set these in a `.env` file in your project directory or as shell environment variables:
56
+
57
+ | Variable | Required | Description |
58
+ |---|---|---|
59
+ | `OPENSERV_USER_API_KEY` | Yes | Your OpenServ API key |
60
+ | `OPENSERV_CONTAINER_ID` | No | Container ID for redeployment (auto-set after first deploy) |
61
+ | `OPENSERV_ORCHESTRATOR_URL` | No | Custom orchestrator URL (defaults to `https://agent-orchestrator.openserv.ai`) |
62
+ | `OPENSERV_API_URL` | No | Custom platform API URL (defaults to `https://api.openserv.ai`) |
63
+
64
+ ### `.openserv.json`
65
+
66
+ Place this file in your project root to link deploys to a registered agent. The CLI reads the first agent entry:
67
+
68
+ ```json
69
+ {
70
+ "agents": {
71
+ "My Agent": {
72
+ "id": 42,
73
+ "apiKey": "your-agent-api-key"
74
+ }
75
+ }
76
+ }
77
+ ```
78
+
79
+ When both `id` and `apiKey` are present, the CLI will automatically update the agent's endpoint URL after going live.
80
+
81
+ ### Files included in the deploy archive
82
+
83
+ The CLI creates a tar.gz archive of your project, applying these rules:
84
+
85
+ - **Always excluded:** `node_modules`, `.git`, `dist`, `.next`, `.turbo`, `.env`, `.env.example`, `.env.local`, `.env.*.local`, `*.tsbuildinfo`
86
+ - **`.gitignore` rules** are respected if a `.gitignore` file exists.
87
+
88
+ ## Publishing to npm
89
+
90
+ ### Prerequisites
91
+
92
+ - An [npm](https://www.npmjs.com/) account with publish access.
93
+ - Node.js 20+.
94
+
95
+ ### Steps
96
+
97
+ 1. **Build the package:**
98
+
99
+ ```bash
100
+ npm run build
101
+ ```
102
+
103
+ 2. **Update the version** (follow [semver](https://semver.org/)):
104
+
105
+ ```bash
106
+ npm version patch # or minor / major
107
+ ```
108
+
109
+ 3. **Publish:**
110
+
111
+ ```bash
112
+ npm publish
113
+ ```
114
+
115
+ The `"files"` field in `package.json` ensures only the `dist/` directory is included in the published package. The `"bin"` field registers the `serv` command globally when installed with `-g`.
116
+
117
+ ## Development
118
+
119
+ ```bash
120
+ # Run locally without building
121
+ npm run dev -- deploy ./my-agent
122
+
123
+ # Build
124
+ npm run build
125
+ ```
package/dist/index.js ADDED
@@ -0,0 +1,392 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/deploy.ts
4
+ import path4 from "path";
5
+
6
+ // src/api-client.ts
7
+ import axios from "axios";
8
+ var DEFAULT_ORCHESTRATOR_URL = "https://agent-orchestrator.openserv.ai";
9
+ var ApiError = class extends Error {
10
+ constructor(message, statusCode) {
11
+ super(message);
12
+ this.statusCode = statusCode;
13
+ this.name = "ApiError";
14
+ }
15
+ };
16
+ var ApiClient = class {
17
+ client;
18
+ constructor(opts) {
19
+ const headers = {
20
+ "x-openserv-key": opts.apiKey
21
+ };
22
+ if (opts.agentId != null) {
23
+ headers["x-openserv-agent-id"] = String(opts.agentId);
24
+ }
25
+ this.client = axios.create({
26
+ baseURL: opts.orchestratorUrl || DEFAULT_ORCHESTRATOR_URL,
27
+ headers,
28
+ maxBodyLength: 100 * 1024 * 1024,
29
+ maxContentLength: 100 * 1024 * 1024
30
+ });
31
+ }
32
+ async createContainer() {
33
+ const res = await this.request("POST", "/container/create");
34
+ return res;
35
+ }
36
+ async getStatus(id) {
37
+ return this.request("GET", `/container/${id}/status`);
38
+ }
39
+ async findContainerByAgent(agentId) {
40
+ try {
41
+ const status = await this.getStatus(String(agentId));
42
+ return {
43
+ id: status.id,
44
+ appName: status.appName,
45
+ machineId: status.machineId,
46
+ status: status.status
47
+ };
48
+ } catch (err) {
49
+ if (err instanceof ApiError && err.statusCode === 404) return null;
50
+ throw err;
51
+ }
52
+ }
53
+ async upload(id, tarBuffer) {
54
+ await this.client.post(`/container/${id}/upload`, tarBuffer, {
55
+ headers: { "Content-Type": "application/gzip" }
56
+ });
57
+ }
58
+ async exec(id, command, timeout) {
59
+ return this.request("POST", `/container/${id}/exec`, {
60
+ command,
61
+ timeout
62
+ });
63
+ }
64
+ async start(id, entrypoint) {
65
+ await this.request("POST", `/container/${id}/start`, {
66
+ entrypoint: entrypoint || "npx tsx src/agent.ts"
67
+ });
68
+ }
69
+ async restart(id) {
70
+ await this.request("POST", `/container/${id}/restart`);
71
+ }
72
+ async goLive(id, mode = "on-demand") {
73
+ return this.request("POST", `/container/${id}/go-live`, {
74
+ mode
75
+ });
76
+ }
77
+ async updateEndpointUrl(agentId, agentApiKey, endpointUrl) {
78
+ const platformUrl = process.env.OPENSERV_API_URL || "https://api.openserv.ai";
79
+ await axios.put(
80
+ `${platformUrl}/agents/${agentId}/endpoint-url`,
81
+ { endpoint_url: endpointUrl },
82
+ { headers: { "x-openserv-key": agentApiKey } }
83
+ );
84
+ }
85
+ async request(method, path5, body) {
86
+ try {
87
+ const res = await this.client.request({
88
+ method,
89
+ url: path5,
90
+ data: body
91
+ });
92
+ return res.data;
93
+ } catch (err) {
94
+ const axiosErr = err;
95
+ const statusCode = axiosErr.response?.status;
96
+ const data = axiosErr.response?.data ?? axiosErr.message;
97
+ const detail = typeof data === "string" ? data : JSON.stringify(data);
98
+ throw new ApiError(
99
+ `${method} ${path5} failed (${statusCode ?? "unknown"}): ${detail}`,
100
+ statusCode
101
+ );
102
+ }
103
+ }
104
+ };
105
+
106
+ // src/env.ts
107
+ import fs from "fs";
108
+ import path from "path";
109
+ import { config as loadDotenv } from "dotenv";
110
+ function readEnv(dir) {
111
+ const envPath = path.join(dir, ".env");
112
+ const parsed = loadDotenv({ path: envPath, override: false });
113
+ const env = parsed.parsed ?? {};
114
+ return {
115
+ apiKey: env.OPENSERV_USER_API_KEY || process.env.OPENSERV_USER_API_KEY,
116
+ containerId: env.OPENSERV_CONTAINER_ID || process.env.OPENSERV_CONTAINER_ID,
117
+ orchestratorUrl: env.OPENSERV_ORCHESTRATOR_URL || process.env.OPENSERV_ORCHESTRATOR_URL
118
+ };
119
+ }
120
+ function writeContainerId(dir, containerId) {
121
+ const envPath = path.join(dir, ".env");
122
+ let content = "";
123
+ if (fs.existsSync(envPath)) {
124
+ content = fs.readFileSync(envPath, "utf8");
125
+ }
126
+ const key = "OPENSERV_CONTAINER_ID";
127
+ const line = `${key}=${containerId}`;
128
+ const regex = new RegExp(`^${key}=.*$`, "m");
129
+ if (regex.test(content)) {
130
+ content = content.replace(regex, line);
131
+ } else {
132
+ const separator = content.length > 0 && !content.endsWith("\n") ? "\n" : "";
133
+ content = `${content}${separator}${line}
134
+ `;
135
+ }
136
+ fs.writeFileSync(envPath, content, "utf8");
137
+ }
138
+
139
+ // src/openserv-json.ts
140
+ import fs2 from "fs";
141
+ import path2 from "path";
142
+ function readAgentConfig(dir) {
143
+ const filePath = path2.join(dir, ".openserv.json");
144
+ if (!fs2.existsSync(filePath)) {
145
+ return void 0;
146
+ }
147
+ try {
148
+ const raw = fs2.readFileSync(filePath, "utf8");
149
+ const data = JSON.parse(raw);
150
+ if (!data.agents) return void 0;
151
+ const firstAgent = Object.values(data.agents)[0];
152
+ if (!firstAgent) return void 0;
153
+ return { id: firstAgent.id, apiKey: firstAgent.apiKey };
154
+ } catch {
155
+ return void 0;
156
+ }
157
+ }
158
+
159
+ // src/tar.ts
160
+ import fs3 from "fs";
161
+ import os from "os";
162
+ import path3 from "path";
163
+ import ignore from "ignore";
164
+ import * as tar from "tar";
165
+ var ALWAYS_EXCLUDE = [
166
+ "node_modules",
167
+ ".git",
168
+ "dist",
169
+ ".next",
170
+ ".turbo",
171
+ ".env",
172
+ ".env.example",
173
+ ".env.local",
174
+ ".env.*.local"
175
+ ];
176
+ var ALWAYS_EXCLUDE_EXTENSIONS = [".tsbuildinfo"];
177
+ async function createTarBuffer(dir) {
178
+ const ig = ignore();
179
+ ig.add(ALWAYS_EXCLUDE);
180
+ const gitignorePath = path3.join(dir, ".gitignore");
181
+ if (fs3.existsSync(gitignorePath)) {
182
+ const content = fs3.readFileSync(gitignorePath, "utf8");
183
+ ig.add(content);
184
+ }
185
+ const entries = collectEntries(dir, dir, ig);
186
+ const tmpFile = path3.join(os.tmpdir(), `serv-deploy-${Date.now()}.tar.gz`);
187
+ try {
188
+ await tar.create(
189
+ {
190
+ gzip: true,
191
+ cwd: dir,
192
+ portable: true,
193
+ file: tmpFile
194
+ },
195
+ entries
196
+ );
197
+ return { buffer: fs3.readFileSync(tmpFile), files: entries };
198
+ } finally {
199
+ try {
200
+ fs3.unlinkSync(tmpFile);
201
+ } catch {
202
+ }
203
+ }
204
+ }
205
+ function collectEntries(baseDir, currentDir, ig) {
206
+ const entries = [];
207
+ const items = fs3.readdirSync(currentDir, { withFileTypes: true });
208
+ for (const item of items) {
209
+ const fullPath = path3.join(currentDir, item.name);
210
+ const relativePath = path3.relative(baseDir, fullPath);
211
+ if (ALWAYS_EXCLUDE_EXTENSIONS.some((ext) => item.name.endsWith(ext))) {
212
+ continue;
213
+ }
214
+ const testPath = item.isDirectory() ? `${relativePath}/` : relativePath;
215
+ if (ig.ignores(testPath)) {
216
+ continue;
217
+ }
218
+ if (item.isDirectory()) {
219
+ entries.push(...collectEntries(baseDir, fullPath, ig));
220
+ } else {
221
+ entries.push(relativePath);
222
+ }
223
+ }
224
+ return entries;
225
+ }
226
+
227
+ // src/deploy.ts
228
+ async function resolveContainer(client, dir, containerId, agentId) {
229
+ if (containerId) {
230
+ console.log(`Using existing container: ${containerId}`);
231
+ return { id: containerId, isFirstDeploy: false };
232
+ }
233
+ if (agentId) {
234
+ console.log(
235
+ `Agent ID found: ${agentId}. Checking for existing container...`
236
+ );
237
+ const existing = await client.findContainerByAgent(agentId);
238
+ if (existing) {
239
+ writeContainerId(dir, existing.id);
240
+ console.log(` Found container: ${existing.id}`);
241
+ console.log(" Saved to .env\n");
242
+ return { id: existing.id, isFirstDeploy: false };
243
+ }
244
+ console.log(" No container found. Creating new container...");
245
+ } else {
246
+ console.log("Creating new container...");
247
+ }
248
+ const container = await client.createContainer();
249
+ writeContainerId(dir, container.id);
250
+ console.log(` Container ID: ${container.id}`);
251
+ console.log(" Written to .env\n");
252
+ return { id: container.id, isFirstDeploy: true };
253
+ }
254
+ async function deploy(targetPath) {
255
+ const dir = path4.resolve(targetPath);
256
+ console.log(`Deploying from ${dir}
257
+ `);
258
+ const env = readEnv(dir);
259
+ const agentConfig = readAgentConfig(dir);
260
+ const agentId = agentConfig?.id;
261
+ if (!env.apiKey) {
262
+ throw new Error(
263
+ "OPENSERV_USER_API_KEY not found. Set it in your .env file or as an environment variable."
264
+ );
265
+ }
266
+ const client = new ApiClient({
267
+ apiKey: env.apiKey,
268
+ agentId,
269
+ orchestratorUrl: env.orchestratorUrl
270
+ });
271
+ const { id: targetId, isFirstDeploy } = await resolveContainer(
272
+ client,
273
+ dir,
274
+ env.containerId,
275
+ agentId
276
+ );
277
+ let currentStatus;
278
+ let appName;
279
+ if (!isFirstDeploy) {
280
+ try {
281
+ const status = await client.getStatus(targetId);
282
+ currentStatus = status.status;
283
+ appName = status.appName;
284
+ console.log(` Current status: ${currentStatus}`);
285
+ } catch {
286
+ }
287
+ }
288
+ console.log("\nCreating archive...");
289
+ const { buffer: tarBuffer, files } = await createTarBuffer(dir);
290
+ for (const file of files) {
291
+ console.log(` ${file}`);
292
+ }
293
+ console.log(
294
+ ` ${files.length} files, ${(tarBuffer.length / 1024).toFixed(1)} KB`
295
+ );
296
+ console.log("\nUploading files...");
297
+ await client.upload(targetId, tarBuffer);
298
+ console.log(" Done.");
299
+ const verify = await client.exec(targetId, ["ls", "-la", "/app"], 30);
300
+ if (verify.exitCode === 0) {
301
+ console.log(" Verified /app contents:");
302
+ for (const line of verify.stdout.split("\n").filter(Boolean)) {
303
+ console.log(` ${line}`);
304
+ }
305
+ } else {
306
+ console.error(" Warning: could not verify upload");
307
+ if (verify.stderr) console.error(` ${verify.stderr}`);
308
+ }
309
+ console.log("\nInstalling dependencies...");
310
+ const installResult = await client.exec(targetId, ["npm", "install"], 600);
311
+ if (installResult.exitCode !== 0) {
312
+ const parts = [`npm install failed (exit code ${installResult.exitCode})`];
313
+ if (installResult.stdout)
314
+ parts.push(`stdout: ${installResult.stdout.slice(0, 500)}`);
315
+ if (installResult.stderr)
316
+ parts.push(`stderr: ${installResult.stderr.slice(0, 500)}`);
317
+ throw new Error(parts.join("\n"));
318
+ }
319
+ console.log(" Done.");
320
+ const needsStart = isFirstDeploy || !currentStatus || currentStatus === "ready";
321
+ if (needsStart) {
322
+ console.log("\nStarting agent...");
323
+ await client.start(targetId);
324
+ console.log(" Agent started.");
325
+ } else {
326
+ console.log("\nRestarting container...");
327
+ await client.restart(targetId);
328
+ console.log(" Container restarted.");
329
+ }
330
+ let publicUrl;
331
+ if (currentStatus !== "live") {
332
+ console.log("\nGoing live...");
333
+ const result = await client.goLive(targetId, "continuous");
334
+ publicUrl = result.publicUrl;
335
+ console.log(` Public URL: ${publicUrl}`);
336
+ } else {
337
+ if (appName) {
338
+ publicUrl = `https://${appName}.fly.dev`;
339
+ }
340
+ console.log("\nAlready live.");
341
+ }
342
+ if (agentConfig?.apiKey && agentConfig.id && publicUrl) {
343
+ console.log("\nUpdating agent endpoint URL...");
344
+ await client.updateEndpointUrl(
345
+ agentConfig.id,
346
+ agentConfig.apiKey,
347
+ publicUrl
348
+ );
349
+ console.log(` Agent endpoint set to ${publicUrl}`);
350
+ }
351
+ console.log("\nDeploy complete!");
352
+ }
353
+
354
+ // src/index.ts
355
+ var HELP = `
356
+ Usage: serv <command> [path]
357
+
358
+ Commands:
359
+ deploy [path] Deploy an agent to OpenServ (default path: .)
360
+
361
+ Options:
362
+ --help, -h Show this help message
363
+
364
+ Environment variables (set in .env or shell):
365
+ OPENSERV_USER_API_KEY Your OpenServ API key (required)
366
+ OPENSERV_CONTAINER_ID Container ID for redeployment (auto-set after first deploy)
367
+ OPENSERV_ORCHESTRATOR_URL Custom orchestrator URL (optional)
368
+ `.trim();
369
+ async function main() {
370
+ const args = process.argv.slice(2);
371
+ if (args.length === 0 || args.includes("--help") || args.includes("-h")) {
372
+ console.log(HELP);
373
+ process.exit(0);
374
+ }
375
+ const command = args[0];
376
+ switch (command) {
377
+ case "deploy": {
378
+ const targetPath = args[1] || ".";
379
+ await deploy(targetPath);
380
+ break;
381
+ }
382
+ default:
383
+ console.error(`Unknown command: ${command}
384
+ `);
385
+ console.log(HELP);
386
+ process.exit(1);
387
+ }
388
+ }
389
+ main().catch((err) => {
390
+ console.error("\nFailed:", err instanceof Error ? err.message : err);
391
+ process.exit(1);
392
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@openserv-labs/deploy",
3
+ "version": "0.1.0",
4
+ "description": "CLI to deploy agents to the OpenServ platform",
5
+ "license": "MIT",
6
+ "keywords": [
7
+ "openserv",
8
+ "agent",
9
+ "deploy",
10
+ "cli"
11
+ ],
12
+ "type": "module",
13
+ "bin": {
14
+ "serv": "./dist/index.js"
15
+ },
16
+ "main": "./dist/index.js",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "dev": "tsx src/index.ts"
26
+ },
27
+ "dependencies": {
28
+ "axios": "^1.7.2",
29
+ "dotenv": "^16.4.7",
30
+ "ignore": "^7.0.3",
31
+ "tar": "^7.4.3"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^20.14.0",
35
+ "tsup": "^8.5.0",
36
+ "tsx": "^4.19.4",
37
+ "typescript": "^5.9.3"
38
+ }
39
+ }