hybrid 2.0.1 → 2.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.
- package/README.md +38 -29
- package/dist/cli.cjs +1701 -643
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1714 -639
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1714 -639
- package/dist/index.js.map +1 -1
- package/dist/server/index.cjs +229318 -17432
- package/package.json +12 -7
- package/skills/skills-manager/SKILL.md +1 -1
- package/templates/agent/.env.example +0 -4
- package/dist/xmtp.cjs +0 -6122
- package/skills/xmtp/SKILL.md +0 -85
package/dist/index.js
CHANGED
|
@@ -1,20 +1,1413 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __esm = (fn, res) => function __init() {
|
|
5
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
6
|
+
};
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
2
11
|
|
|
3
|
-
//
|
|
4
|
-
import
|
|
5
|
-
import { basename, dirname, resolve } from "path";
|
|
12
|
+
// ../../node_modules/.pnpm/tsup@8.5.0_jiti@2.6.0_postcss@8.5.6_tsx@4.20.5_typescript@5.9.3_yaml@2.8.1/node_modules/tsup/assets/esm_shims.js
|
|
13
|
+
import path from "path";
|
|
6
14
|
import { fileURLToPath } from "url";
|
|
15
|
+
var init_esm_shims = __esm({
|
|
16
|
+
"../../node_modules/.pnpm/tsup@8.5.0_jiti@2.6.0_postcss@8.5.6_tsx@4.20.5_typescript@5.9.3_yaml@2.8.1/node_modules/tsup/assets/esm_shims.js"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// src/deploy/providers/sprite.provider.ts
|
|
22
|
+
var sprite_provider_exports = {};
|
|
23
|
+
__export(sprite_provider_exports, {
|
|
24
|
+
spriteProvider: () => spriteProvider
|
|
25
|
+
});
|
|
26
|
+
import { execFileSync, spawn } from "child_process";
|
|
27
|
+
import { existsSync, unlinkSync } from "fs";
|
|
28
|
+
import { tmpdir } from "os";
|
|
29
|
+
import { basename, join } from "path";
|
|
30
|
+
var spriteProvider;
|
|
31
|
+
var init_sprite_provider = __esm({
|
|
32
|
+
"src/deploy/providers/sprite.provider.ts"() {
|
|
33
|
+
"use strict";
|
|
34
|
+
init_esm_shims();
|
|
35
|
+
spriteProvider = {
|
|
36
|
+
name: "sprites",
|
|
37
|
+
label: "Sprites.dev (Firecracker)",
|
|
38
|
+
defaultName(projectDir) {
|
|
39
|
+
return basename(projectDir).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
40
|
+
},
|
|
41
|
+
async authCheck() {
|
|
42
|
+
try {
|
|
43
|
+
execFileSync("sprite", ["list"], { stdio: "pipe", timeout: 5e3 });
|
|
44
|
+
} catch (err) {
|
|
45
|
+
if (err.code === "ENOENT") {
|
|
46
|
+
throw new Error(
|
|
47
|
+
"'sprite' CLI not found.\n Install: https://sprites.dev/docs/install\n Or: npm i -g sprite-cli"
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
throw new Error(
|
|
51
|
+
`Sprite CLI authentication failed.
|
|
52
|
+
Run: sprite login
|
|
53
|
+
Error: ${err.stderr || err.message}`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
async provision(name, _opts) {
|
|
58
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]*$/.test(name)) {
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Invalid sprite name: ${name}
|
|
61
|
+
Name must start with a letter/digit and contain only letters, digits, hyphens, and underscores.`
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const existing = execFileSync("sprite", ["list"], {
|
|
66
|
+
encoding: "utf-8"
|
|
67
|
+
});
|
|
68
|
+
if (existing.includes(name)) {
|
|
69
|
+
return name;
|
|
70
|
+
}
|
|
71
|
+
} catch {
|
|
72
|
+
}
|
|
73
|
+
console.log(`
|
|
74
|
+
\u{1F4E6} Creating sprite: ${name}`);
|
|
75
|
+
try {
|
|
76
|
+
execFileSync("sprite", ["create", "-skip-console", name], {
|
|
77
|
+
stdio: "inherit"
|
|
78
|
+
});
|
|
79
|
+
} catch {
|
|
80
|
+
const existing = execFileSync("sprite", ["list"], {
|
|
81
|
+
encoding: "utf-8"
|
|
82
|
+
});
|
|
83
|
+
if (existing.includes(name)) {
|
|
84
|
+
console.log(` Sprite ${name} already exists, using it`);
|
|
85
|
+
return name;
|
|
86
|
+
}
|
|
87
|
+
throw new Error("Failed to create sprite");
|
|
88
|
+
}
|
|
89
|
+
console.log("\n\u23F3 Waiting for sprite to be ready...");
|
|
90
|
+
for (let i = 0; i < 30; i++) {
|
|
91
|
+
try {
|
|
92
|
+
execFileSync("sprite", ["exec", "-s", name, "--", "echo", "ready"], {
|
|
93
|
+
stdio: "pipe"
|
|
94
|
+
});
|
|
95
|
+
console.log(" \u2713 Sprite ready");
|
|
96
|
+
return name;
|
|
97
|
+
} catch {
|
|
98
|
+
if (i % 5 === 4) {
|
|
99
|
+
console.log(` Still waiting... (${i + 1}/30)`);
|
|
100
|
+
}
|
|
101
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
throw new Error("Sprite did not become ready in time");
|
|
105
|
+
},
|
|
106
|
+
async deploy(instanceId, distDir) {
|
|
107
|
+
console.log("\n\u{1F4E4} Uploading build artifacts...");
|
|
108
|
+
const tarPath = join(tmpdir(), `hybrid-deploy-${Date.now()}.tar.gz`);
|
|
109
|
+
execFileSync("tar", ["-czf", tarPath, "-C", distDir, "."], {
|
|
110
|
+
stdio: "pipe"
|
|
111
|
+
});
|
|
112
|
+
execFileSync(
|
|
113
|
+
"sprite",
|
|
114
|
+
["exec", "-s", instanceId, "--", "mkdir", "-p", "/app"],
|
|
115
|
+
{
|
|
116
|
+
stdio: "pipe"
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
let uploadSuccess = false;
|
|
120
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
121
|
+
try {
|
|
122
|
+
execFileSync(
|
|
123
|
+
"sprite",
|
|
124
|
+
[
|
|
125
|
+
"exec",
|
|
126
|
+
"-s",
|
|
127
|
+
instanceId,
|
|
128
|
+
"-file",
|
|
129
|
+
`${tarPath}:/tmp/hybrid-deploy.tar.gz`,
|
|
130
|
+
"--",
|
|
131
|
+
"tar",
|
|
132
|
+
"-xzf",
|
|
133
|
+
"/tmp/hybrid-deploy.tar.gz",
|
|
134
|
+
"-C",
|
|
135
|
+
"/app"
|
|
136
|
+
],
|
|
137
|
+
{ stdio: "inherit" }
|
|
138
|
+
);
|
|
139
|
+
uploadSuccess = true;
|
|
140
|
+
break;
|
|
141
|
+
} catch {
|
|
142
|
+
if (attempt < 2) {
|
|
143
|
+
console.log(` Upload failed, retrying... (${attempt + 1}/3)`);
|
|
144
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (existsSync(tarPath)) {
|
|
149
|
+
try {
|
|
150
|
+
unlinkSync(tarPath);
|
|
151
|
+
} catch {
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!uploadSuccess) {
|
|
155
|
+
throw new Error("Failed to upload build artifacts after 3 attempts");
|
|
156
|
+
}
|
|
157
|
+
console.log("\n\u{1F4E6} Installing dependencies...");
|
|
158
|
+
execFileSync(
|
|
159
|
+
"sprite",
|
|
160
|
+
[
|
|
161
|
+
"exec",
|
|
162
|
+
"-s",
|
|
163
|
+
instanceId,
|
|
164
|
+
"--",
|
|
165
|
+
"bash",
|
|
166
|
+
"-c",
|
|
167
|
+
"cd /app && npm install --production"
|
|
168
|
+
],
|
|
169
|
+
{ stdio: "inherit" }
|
|
170
|
+
);
|
|
171
|
+
console.log("\n\u{1F527} Starting agent...");
|
|
172
|
+
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
173
|
+
const scriptPath = join(tmpdir(), `hybrid-start-${Date.now()}.sh`);
|
|
174
|
+
const startupScript = `#!/bin/bash
|
|
175
|
+
cd /app
|
|
176
|
+
export NODE_ENV=production
|
|
177
|
+
export AGENT_PORT=8454
|
|
178
|
+
exec nohup node server/index.cjs > /app/agent.log 2>&1 &
|
|
179
|
+
echo $! > /app/agent.pid
|
|
180
|
+
`;
|
|
181
|
+
writeFileSync2(scriptPath, startupScript, { mode: 493 });
|
|
182
|
+
execFileSync(
|
|
183
|
+
"sprite",
|
|
184
|
+
[
|
|
185
|
+
"exec",
|
|
186
|
+
"-s",
|
|
187
|
+
instanceId,
|
|
188
|
+
"-file",
|
|
189
|
+
`${scriptPath}:/app/start-agent.sh`,
|
|
190
|
+
"--",
|
|
191
|
+
"chmod",
|
|
192
|
+
"+x",
|
|
193
|
+
"/app/start-agent.sh"
|
|
194
|
+
],
|
|
195
|
+
{ stdio: "pipe" }
|
|
196
|
+
);
|
|
197
|
+
execFileSync(
|
|
198
|
+
"sprite",
|
|
199
|
+
["exec", "-s", instanceId, "--", "bash", "/app/start-agent.sh"],
|
|
200
|
+
{ stdio: "inherit" }
|
|
201
|
+
);
|
|
202
|
+
try {
|
|
203
|
+
unlinkSync(scriptPath);
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
console.log("\n\u23F3 Waiting for agent to start...");
|
|
207
|
+
let ready = false;
|
|
208
|
+
for (let i = 0; i < 15; i++) {
|
|
209
|
+
try {
|
|
210
|
+
const result = execFileSync(
|
|
211
|
+
"sprite",
|
|
212
|
+
[
|
|
213
|
+
"exec",
|
|
214
|
+
"-s",
|
|
215
|
+
instanceId,
|
|
216
|
+
"--",
|
|
217
|
+
"curl",
|
|
218
|
+
"-s",
|
|
219
|
+
"http://localhost:8454/health"
|
|
220
|
+
],
|
|
221
|
+
{ encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] }
|
|
222
|
+
);
|
|
223
|
+
if (result) {
|
|
224
|
+
ready = true;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
} catch {
|
|
228
|
+
}
|
|
229
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
230
|
+
}
|
|
231
|
+
if (!ready) {
|
|
232
|
+
console.log(" \u26A0\uFE0F Agent may not be fully ready (health check timed out)");
|
|
233
|
+
} else {
|
|
234
|
+
console.log(" \u2713 Agent running");
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
async status(instanceId) {
|
|
238
|
+
try {
|
|
239
|
+
const output = execFileSync("sprite", ["list"], {
|
|
240
|
+
encoding: "utf-8"
|
|
241
|
+
});
|
|
242
|
+
if (!output.includes(instanceId)) {
|
|
243
|
+
return "stopped";
|
|
244
|
+
}
|
|
245
|
+
const lines = output.split("\n");
|
|
246
|
+
for (const line of lines) {
|
|
247
|
+
if (line.includes(instanceId)) {
|
|
248
|
+
if (line.includes("sleeping") || line.includes("paused")) {
|
|
249
|
+
return "sleeping";
|
|
250
|
+
}
|
|
251
|
+
if (line.includes("running")) {
|
|
252
|
+
return "running";
|
|
253
|
+
}
|
|
254
|
+
if (line.includes("stopped") || line.includes("terminated")) {
|
|
255
|
+
return "stopped";
|
|
256
|
+
}
|
|
257
|
+
return "running";
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return "unknown";
|
|
261
|
+
} catch {
|
|
262
|
+
return "unknown";
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
async sleep(instanceId) {
|
|
266
|
+
console.log(`
|
|
267
|
+
\u{1F4A4} Sleeping sprite: ${instanceId}`);
|
|
268
|
+
try {
|
|
269
|
+
execFileSync("sprite", ["sleep", instanceId], { stdio: "inherit" });
|
|
270
|
+
console.log(" \u2713 Sprite sleeping");
|
|
271
|
+
} catch (err) {
|
|
272
|
+
throw new Error(`Failed to sleep sprite: ${err.stderr || err.message}`);
|
|
273
|
+
}
|
|
274
|
+
},
|
|
275
|
+
async wake(instanceId) {
|
|
276
|
+
console.log(`
|
|
277
|
+
\u2600\uFE0F Waking sprite: ${instanceId}`);
|
|
278
|
+
try {
|
|
279
|
+
execFileSync("sprite", ["exec", "-s", instanceId, "--", "echo", "wake"], {
|
|
280
|
+
stdio: "pipe"
|
|
281
|
+
});
|
|
282
|
+
console.log(" \u2713 Sprite awake");
|
|
283
|
+
} catch (err) {
|
|
284
|
+
throw new Error(`Failed to wake sprite: ${err.stderr || err.message}`);
|
|
285
|
+
}
|
|
286
|
+
},
|
|
287
|
+
async logs(instanceId, follow = true) {
|
|
288
|
+
const args = follow ? ["logs", "-s", instanceId, "-f"] : ["logs", "-s", instanceId];
|
|
289
|
+
const child = spawn("sprite", args, { stdio: "inherit" });
|
|
290
|
+
return new Promise((resolve3, reject) => {
|
|
291
|
+
child.on("exit", (code) => {
|
|
292
|
+
if (code === 0) resolve3();
|
|
293
|
+
else reject(new Error(`sprite logs exited with code ${code}`));
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
},
|
|
297
|
+
async endpoint(instanceId) {
|
|
298
|
+
return `https://${instanceId}.sprites.dev`;
|
|
299
|
+
},
|
|
300
|
+
async teardown(instanceId) {
|
|
301
|
+
console.log(`
|
|
302
|
+
\u{1F5D1}\uFE0F Destroying sprite: ${instanceId}`);
|
|
303
|
+
try {
|
|
304
|
+
execFileSync("sprite", ["delete", instanceId], { stdio: "inherit" });
|
|
305
|
+
console.log(" \u2713 Sprite destroyed");
|
|
306
|
+
} catch (err) {
|
|
307
|
+
throw new Error(`Failed to destroy sprite: ${err.stderr || err.message}`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
// src/deploy/providers/e2b.provider.ts
|
|
315
|
+
var e2b_provider_exports = {};
|
|
316
|
+
__export(e2b_provider_exports, {
|
|
317
|
+
e2bProvider: () => e2bProvider
|
|
318
|
+
});
|
|
319
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
320
|
+
import { existsSync as existsSync2, unlinkSync as unlinkSync2 } from "fs";
|
|
321
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
322
|
+
import { basename as basename2, join as join2 } from "path";
|
|
323
|
+
async function findSandbox(name) {
|
|
324
|
+
const Sandbox = await getSDK();
|
|
325
|
+
const paginator = Sandbox.list();
|
|
326
|
+
while (paginator.hasNext) {
|
|
327
|
+
const items = await paginator.nextItems();
|
|
328
|
+
for (const s of items) {
|
|
329
|
+
const info = s;
|
|
330
|
+
const meta = info.metadata;
|
|
331
|
+
if (meta?.["hybrid-name"] === name || info.sandboxId === name || info.sandboxId?.startsWith(name)) {
|
|
332
|
+
return s;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
async function getSDK() {
|
|
339
|
+
try {
|
|
340
|
+
const { Sandbox } = await import("e2b");
|
|
341
|
+
return Sandbox;
|
|
342
|
+
} catch {
|
|
343
|
+
throw new Error(
|
|
344
|
+
"e2b not installed.\nInstall with: npm install e2b"
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
function getAPIKey() {
|
|
349
|
+
const key = process.env.E2B_API_KEY;
|
|
350
|
+
if (!key) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
"E2B_API_KEY not set.\nGet your key: https://e2b.dev/dashboard?tab=keys\nSet with: export E2B_API_KEY=e2b_xxx"
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
return key;
|
|
356
|
+
}
|
|
357
|
+
async function getSandbox(name) {
|
|
358
|
+
const cached = sandboxes.get(name);
|
|
359
|
+
if (cached) return cached;
|
|
360
|
+
const found = await findSandbox(name);
|
|
361
|
+
if (!found) return null;
|
|
362
|
+
const Sandbox = await getSDK();
|
|
363
|
+
const sandbox = await Sandbox.connect(found.sandboxId);
|
|
364
|
+
sandboxes.set(name, sandbox);
|
|
365
|
+
return sandbox;
|
|
366
|
+
}
|
|
367
|
+
var DEFAULT_TEMPLATE, sandboxes, e2bProvider;
|
|
368
|
+
var init_e2b_provider = __esm({
|
|
369
|
+
"src/deploy/providers/e2b.provider.ts"() {
|
|
370
|
+
"use strict";
|
|
371
|
+
init_esm_shims();
|
|
372
|
+
DEFAULT_TEMPLATE = "base";
|
|
373
|
+
sandboxes = /* @__PURE__ */ new Map();
|
|
374
|
+
e2bProvider = {
|
|
375
|
+
name: "e2b",
|
|
376
|
+
label: "E2B.dev (Firecracker)",
|
|
377
|
+
defaultName(projectDir) {
|
|
378
|
+
return basename2(projectDir).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
379
|
+
},
|
|
380
|
+
async authCheck() {
|
|
381
|
+
getAPIKey();
|
|
382
|
+
const Sandbox = await getSDK();
|
|
383
|
+
try {
|
|
384
|
+
const paginator = Sandbox.list();
|
|
385
|
+
await paginator.nextItems();
|
|
386
|
+
} catch (err) {
|
|
387
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
388
|
+
if (msg.includes("401") || msg.includes("unauthorized")) {
|
|
389
|
+
throw new Error(
|
|
390
|
+
"E2B API key is invalid.\nGet your key: https://e2b.dev/dashboard?tab=keys"
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
throw new Error(`E2B connection failed: ${msg}`);
|
|
394
|
+
}
|
|
395
|
+
},
|
|
396
|
+
async provision(name, _opts) {
|
|
397
|
+
const Sandbox = await getSDK();
|
|
398
|
+
console.log(`
|
|
399
|
+
\u{1F4E6} Creating E2B sandbox: ${name}`);
|
|
400
|
+
const existing = await findSandbox(name);
|
|
401
|
+
if (existing) {
|
|
402
|
+
const sandbox2 = await Sandbox.connect(
|
|
403
|
+
existing.sandboxId
|
|
404
|
+
);
|
|
405
|
+
sandboxes.set(name, sandbox2);
|
|
406
|
+
console.log(
|
|
407
|
+
` \u2713 Resumed existing sandbox: ${existing.sandboxId}`
|
|
408
|
+
);
|
|
409
|
+
return existing.sandboxId;
|
|
410
|
+
}
|
|
411
|
+
const sandbox = await Sandbox.create(DEFAULT_TEMPLATE, {
|
|
412
|
+
metadata: { "hybrid-name": name }
|
|
413
|
+
});
|
|
414
|
+
sandboxes.set(name, sandbox);
|
|
415
|
+
console.log(` \u2713 Sandbox created: ${sandbox.sandboxId}`);
|
|
416
|
+
return sandbox.sandboxId;
|
|
417
|
+
},
|
|
418
|
+
async deploy(instanceId, distDir) {
|
|
419
|
+
const sandbox = await getSandbox(instanceId);
|
|
420
|
+
if (!sandbox) {
|
|
421
|
+
throw new Error(
|
|
422
|
+
`Sandbox ${instanceId} not found.
|
|
423
|
+
Run 'hybrid deploy' again to reconnect.`
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
console.log("\n\u{1F4E4} Uploading build artifacts...");
|
|
427
|
+
const tarPath = join2(
|
|
428
|
+
tmpdir2(),
|
|
429
|
+
`hybrid-e2b-deploy-${Date.now()}.tar.gz`
|
|
430
|
+
);
|
|
431
|
+
execFileSync2("tar", ["-czf", tarPath, "-C", distDir, "."], {
|
|
432
|
+
stdio: "pipe"
|
|
433
|
+
});
|
|
434
|
+
try {
|
|
435
|
+
const fs = await import("fs/promises");
|
|
436
|
+
const tarBuffer = await fs.readFile(tarPath);
|
|
437
|
+
await sandbox.files.write("/tmp/hybrid-deploy.tar.gz", tarBuffer, {
|
|
438
|
+
onProgress: () => {
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
console.log(" \u2713 Upload complete");
|
|
442
|
+
} catch (err) {
|
|
443
|
+
throw new Error(
|
|
444
|
+
`Upload failed: ${err instanceof Error ? err.message : String(err)}`
|
|
445
|
+
);
|
|
446
|
+
} finally {
|
|
447
|
+
if (existsSync2(tarPath)) {
|
|
448
|
+
try {
|
|
449
|
+
unlinkSync2(tarPath);
|
|
450
|
+
} catch {
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
console.log("\n\u{1F4E6} Extracting and installing dependencies...");
|
|
455
|
+
const result = await sandbox.commands.run(
|
|
456
|
+
"cd /home/user && mkdir -p app && tar -xzf /tmp/hybrid-deploy.tar.gz -C app && cd app && npm install --production 2>&1",
|
|
457
|
+
{ timeout: 12e4 }
|
|
458
|
+
);
|
|
459
|
+
if (result.stderr) {
|
|
460
|
+
console.log(` \u26A0\uFE0F ${result.stderr.slice(0, 200)}`);
|
|
461
|
+
}
|
|
462
|
+
if (result.exitCode !== 0) {
|
|
463
|
+
throw new Error(
|
|
464
|
+
`Dependency installation failed (exit ${result.exitCode})`
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
console.log("\n\u{1F527} Starting agent...");
|
|
468
|
+
await sandbox.commands.run(
|
|
469
|
+
"cd /home/user/app && export NODE_ENV=production AGENT_PORT=8454 && nohup node server/index.cjs > /tmp/agent.log 2>&1 & echo $! > /tmp/agent.pid",
|
|
470
|
+
{ timeout: 1e4 }
|
|
471
|
+
);
|
|
472
|
+
console.log("\n\u23F3 Waiting for agent to start...");
|
|
473
|
+
let ready = false;
|
|
474
|
+
for (let i = 0; i < 15; i++) {
|
|
475
|
+
try {
|
|
476
|
+
const health = await sandbox.commands.run(
|
|
477
|
+
"curl -sf http://localhost:8454/health || echo 'not ready'"
|
|
478
|
+
);
|
|
479
|
+
if (health.stdout && !health.stdout.includes("not ready")) {
|
|
480
|
+
ready = true;
|
|
481
|
+
break;
|
|
482
|
+
}
|
|
483
|
+
} catch {
|
|
484
|
+
}
|
|
485
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
486
|
+
}
|
|
487
|
+
if (!ready) {
|
|
488
|
+
console.log(
|
|
489
|
+
" \u26A0\uFE0F Agent health check timed out (may still be starting)"
|
|
490
|
+
);
|
|
491
|
+
} else {
|
|
492
|
+
console.log(" \u2713 Agent running");
|
|
493
|
+
}
|
|
494
|
+
},
|
|
495
|
+
async status(instanceId) {
|
|
496
|
+
const info = await findSandbox(instanceId);
|
|
497
|
+
if (!info) return "stopped";
|
|
498
|
+
const sbInfo = info;
|
|
499
|
+
if (sbInfo.status === "paused") return "sleeping";
|
|
500
|
+
if (sbInfo.status === "running") return "running";
|
|
501
|
+
return "unknown";
|
|
502
|
+
},
|
|
503
|
+
async sleep(instanceId) {
|
|
504
|
+
const sandbox = await getSandbox(instanceId);
|
|
505
|
+
if (!sandbox) {
|
|
506
|
+
throw new Error(
|
|
507
|
+
`Sandbox ${instanceId} not found.
|
|
508
|
+
Run 'hybrid deploy' again to reconnect.`
|
|
509
|
+
);
|
|
510
|
+
}
|
|
511
|
+
console.log(`
|
|
512
|
+
\u{1F4A4} Pausing E2B sandbox: ${instanceId}`);
|
|
513
|
+
await sandbox.pause();
|
|
514
|
+
sandboxes.delete(instanceId);
|
|
515
|
+
console.log(" \u2713 Sandbox paused (state preserved on disk)");
|
|
516
|
+
},
|
|
517
|
+
async wake(instanceId) {
|
|
518
|
+
const Sandbox = await getSDK();
|
|
519
|
+
console.log(`
|
|
520
|
+
\u2600\uFE0F Resuming E2B sandbox: ${instanceId}`);
|
|
521
|
+
try {
|
|
522
|
+
const sandbox = await Sandbox.connect(instanceId);
|
|
523
|
+
sandboxes.set(instanceId, sandbox);
|
|
524
|
+
console.log(" \u2713 Sandbox resumed");
|
|
525
|
+
} catch (err) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`Failed to resume sandbox: ${err instanceof Error ? err.message : String(err)}
|
|
528
|
+
The sandbox may have expired (E2B sandboxes have a maximum lifetime).`
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
},
|
|
532
|
+
async logs(instanceId, follow = true) {
|
|
533
|
+
const sandbox = await getSandbox(instanceId);
|
|
534
|
+
if (!sandbox) {
|
|
535
|
+
throw new Error(
|
|
536
|
+
`Sandbox ${instanceId} not found.
|
|
537
|
+
Run 'hybrid deploy' again to reconnect.`
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
if (follow) {
|
|
541
|
+
let offset = 0;
|
|
542
|
+
while (true) {
|
|
543
|
+
try {
|
|
544
|
+
const result = await sandbox.commands.run(
|
|
545
|
+
"cat /tmp/agent.log 2>/dev/null || echo 'No logs yet'"
|
|
546
|
+
);
|
|
547
|
+
const output = result.stdout || "";
|
|
548
|
+
if (output.length > offset) {
|
|
549
|
+
process.stdout.write(output.slice(offset));
|
|
550
|
+
offset = output.length;
|
|
551
|
+
}
|
|
552
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
553
|
+
} catch {
|
|
554
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
} else {
|
|
558
|
+
const result = await sandbox.commands.run(
|
|
559
|
+
"cat /tmp/agent.log 2>/dev/null || echo 'No logs yet'"
|
|
560
|
+
);
|
|
561
|
+
console.log(result.stdout || "No logs yet");
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
async endpoint(instanceId) {
|
|
565
|
+
const sandbox = sandboxes.get(instanceId);
|
|
566
|
+
if (!sandbox) return `https://${instanceId}-8454.e2b.dev`;
|
|
567
|
+
try {
|
|
568
|
+
const host = await sandbox.getHost(8454);
|
|
569
|
+
return `https://${host}`;
|
|
570
|
+
} catch {
|
|
571
|
+
return `https://${instanceId}-8454.e2b.dev`;
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
async teardown(instanceId) {
|
|
575
|
+
const Sandbox = await getSDK();
|
|
576
|
+
console.log(`
|
|
577
|
+
\u{1F5D1}\uFE0F Destroying E2B sandbox: ${instanceId}`);
|
|
578
|
+
try {
|
|
579
|
+
const cached = sandboxes.get(instanceId);
|
|
580
|
+
if (cached) {
|
|
581
|
+
await cached.kill();
|
|
582
|
+
} else {
|
|
583
|
+
const sandbox = await Sandbox.connect(instanceId);
|
|
584
|
+
await sandbox.kill();
|
|
585
|
+
}
|
|
586
|
+
sandboxes.delete(instanceId);
|
|
587
|
+
console.log(" \u2713 Sandbox destroyed");
|
|
588
|
+
} catch (err) {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`Failed to destroy sandbox: ${err instanceof Error ? err.message : String(err)}`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
// src/deploy/providers/northflank.provider.ts
|
|
599
|
+
var northflank_provider_exports = {};
|
|
600
|
+
__export(northflank_provider_exports, {
|
|
601
|
+
northflankProvider: () => northflankProvider
|
|
602
|
+
});
|
|
603
|
+
import { execFileSync as execFileSync3, spawn as spawn2 } from "child_process";
|
|
604
|
+
import { existsSync as existsSync3, unlinkSync as unlinkSync3 } from "fs";
|
|
605
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
606
|
+
import { basename as basename3, join as join3 } from "path";
|
|
607
|
+
function runNf(args) {
|
|
608
|
+
return execFileSync3(CLI, args, {
|
|
609
|
+
encoding: "utf-8",
|
|
610
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
function runNfInherit(args) {
|
|
614
|
+
execFileSync3(CLI, args, { stdio: "inherit" });
|
|
615
|
+
}
|
|
616
|
+
var CLI, northflankProvider;
|
|
617
|
+
var init_northflank_provider = __esm({
|
|
618
|
+
"src/deploy/providers/northflank.provider.ts"() {
|
|
619
|
+
"use strict";
|
|
620
|
+
init_esm_shims();
|
|
621
|
+
CLI = "nf";
|
|
622
|
+
northflankProvider = {
|
|
623
|
+
name: "northflank",
|
|
624
|
+
label: "Northflank (Firecracker / Auto-scale)",
|
|
625
|
+
defaultName(projectDir) {
|
|
626
|
+
return basename3(projectDir).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
627
|
+
},
|
|
628
|
+
async authCheck() {
|
|
629
|
+
try {
|
|
630
|
+
const result = runNf(["auth", "whoami"]);
|
|
631
|
+
if (!result || result.includes("not logged in")) {
|
|
632
|
+
throw new Error(
|
|
633
|
+
"Not authenticated with Northflank.\n Run: nf auth login"
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
} catch (err) {
|
|
637
|
+
if (err.code === "ENOENT") {
|
|
638
|
+
throw new Error(
|
|
639
|
+
"`nf` CLI not found.\n Install: https://docs.northflank.com/docs/cli\n Or: npm install -g @northflank/cli"
|
|
640
|
+
);
|
|
641
|
+
}
|
|
642
|
+
throw new Error(
|
|
643
|
+
`Northflank CLI error: ${err.stderr || err.message}
|
|
644
|
+
Run: nf auth login`
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
},
|
|
648
|
+
async provision(name, opts) {
|
|
649
|
+
const projectId = process.env.NF_PROJECT_ID;
|
|
650
|
+
if (!projectId) {
|
|
651
|
+
throw new Error(
|
|
652
|
+
"NF_PROJECT_ID not set.\n Create a project on Northflank and set: export NF_PROJECT_ID=your-project-id"
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
try {
|
|
656
|
+
const existing = runNf(["services", "list", "--projectId", projectId]);
|
|
657
|
+
if (existing.toLowerCase().includes(name)) {
|
|
658
|
+
console.log(` \u2713 Service already exists: ${name}`);
|
|
659
|
+
return name;
|
|
660
|
+
}
|
|
661
|
+
} catch {
|
|
662
|
+
}
|
|
663
|
+
const apiUrl = process.env.NF_API_URL || "https://api.northflank.com";
|
|
664
|
+
const memory = opts?.memory || 512;
|
|
665
|
+
const cpus = opts?.cpus || 1;
|
|
666
|
+
console.log(`
|
|
667
|
+
\u{1F4E6} Creating Northflank service: ${name}`);
|
|
668
|
+
console.log(` Project: ${projectId}`);
|
|
669
|
+
console.log(` Memory: ${memory}MB, CPUs: ${cpus}`);
|
|
670
|
+
const spec = {
|
|
671
|
+
name,
|
|
672
|
+
projectId,
|
|
673
|
+
type: "service",
|
|
674
|
+
image: {
|
|
675
|
+
image: `registry.northflank.com/${projectId}/${name}:latest`
|
|
676
|
+
},
|
|
677
|
+
resources: {
|
|
678
|
+
instances: 1,
|
|
679
|
+
containerResources: {
|
|
680
|
+
cpu: {
|
|
681
|
+
req: cpus,
|
|
682
|
+
limit: cpus
|
|
683
|
+
},
|
|
684
|
+
memory: {
|
|
685
|
+
req: memory,
|
|
686
|
+
limit: memory
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
},
|
|
690
|
+
ports: [
|
|
691
|
+
{
|
|
692
|
+
name: "default",
|
|
693
|
+
protocol: "HTTP",
|
|
694
|
+
containerPort: 8454,
|
|
695
|
+
ingress: {
|
|
696
|
+
autoSubdomain: true
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
],
|
|
700
|
+
autoscaling: {
|
|
701
|
+
enabled: true,
|
|
702
|
+
minReplicas: 0,
|
|
703
|
+
maxReplicas: 1
|
|
704
|
+
}
|
|
705
|
+
};
|
|
706
|
+
const specPath = join3(tmpdir3(), `nf-spec-${Date.now()}.json`);
|
|
707
|
+
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
708
|
+
writeFileSync2(specPath, JSON.stringify(spec, null, 2));
|
|
709
|
+
try {
|
|
710
|
+
runNfInherit(["deploy", "service", specPath]);
|
|
711
|
+
} finally {
|
|
712
|
+
if (existsSync3(specPath)) {
|
|
713
|
+
try {
|
|
714
|
+
unlinkSync3(specPath);
|
|
715
|
+
} catch {
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
console.log(" \u2713 Service created with auto-scale-to-zero enabled");
|
|
720
|
+
return name;
|
|
721
|
+
},
|
|
722
|
+
async deploy(instanceId, distDir) {
|
|
723
|
+
const projectId = process.env.NF_PROJECT_ID;
|
|
724
|
+
if (!projectId) {
|
|
725
|
+
throw new Error("NF_PROJECT_ID not set.");
|
|
726
|
+
}
|
|
727
|
+
console.log("\n\u{1F4E6} Building and pushing Docker image...");
|
|
728
|
+
const imageTag = `registry.northflank.com/${projectId}/${instanceId}:latest`;
|
|
729
|
+
runNf(["info"]);
|
|
730
|
+
console.log(" Building Docker image...");
|
|
731
|
+
execFileSync3("docker", ["build", "-t", imageTag, "."], {
|
|
732
|
+
cwd: distDir,
|
|
733
|
+
stdio: "inherit"
|
|
734
|
+
});
|
|
735
|
+
console.log(" Pushing to Northflank registry...");
|
|
736
|
+
execFileSync3("docker", ["push", imageTag], {
|
|
737
|
+
stdio: "inherit"
|
|
738
|
+
});
|
|
739
|
+
console.log("\n\u{1F527} Deploying service...");
|
|
740
|
+
try {
|
|
741
|
+
runNfInherit([
|
|
742
|
+
"services",
|
|
743
|
+
"set-image",
|
|
744
|
+
"--projectId",
|
|
745
|
+
projectId,
|
|
746
|
+
"--serviceId",
|
|
747
|
+
instanceId,
|
|
748
|
+
"--image",
|
|
749
|
+
imageTag
|
|
750
|
+
]);
|
|
751
|
+
} catch (err) {
|
|
752
|
+
console.log(" \u26A0\uFE0F Service update initiated");
|
|
753
|
+
}
|
|
754
|
+
console.log("\n\u23F3 Waiting for deployment to be ready...");
|
|
755
|
+
for (let i = 0; i < 60; i++) {
|
|
756
|
+
try {
|
|
757
|
+
const status = runNf([
|
|
758
|
+
"services",
|
|
759
|
+
"status",
|
|
760
|
+
"--projectId",
|
|
761
|
+
projectId,
|
|
762
|
+
"--serviceId",
|
|
763
|
+
instanceId
|
|
764
|
+
]);
|
|
765
|
+
if (status.toLowerCase().includes("deployed") || status.toLowerCase().includes("running")) {
|
|
766
|
+
console.log(" \u2713 Service deployed");
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
769
|
+
} catch {
|
|
770
|
+
}
|
|
771
|
+
if (i % 10 === 9) {
|
|
772
|
+
console.log(` Still deploying... (${i + 1}/60)`);
|
|
773
|
+
}
|
|
774
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
775
|
+
}
|
|
776
|
+
console.log(" \u26A0\uFE0F Deployment may still be in progress");
|
|
777
|
+
},
|
|
778
|
+
async status(instanceId) {
|
|
779
|
+
const projectId = process.env.NF_PROJECT_ID;
|
|
780
|
+
if (!projectId) return "unknown";
|
|
781
|
+
try {
|
|
782
|
+
const status = runNf([
|
|
783
|
+
"services",
|
|
784
|
+
"status",
|
|
785
|
+
"--projectId",
|
|
786
|
+
projectId,
|
|
787
|
+
"--serviceId",
|
|
788
|
+
instanceId
|
|
789
|
+
]);
|
|
790
|
+
const lower = status.toLowerCase();
|
|
791
|
+
if (lower.includes("deployed") || lower.includes("running")) {
|
|
792
|
+
return "running";
|
|
793
|
+
}
|
|
794
|
+
if (lower.includes("stopped") || lower.includes("paused")) {
|
|
795
|
+
return "sleeping";
|
|
796
|
+
}
|
|
797
|
+
if (lower.includes("deploying")) {
|
|
798
|
+
return "provisioning";
|
|
799
|
+
}
|
|
800
|
+
return "unknown";
|
|
801
|
+
} catch {
|
|
802
|
+
return "stopped";
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
async sleep(instanceId) {
|
|
806
|
+
const projectId = process.env.NF_PROJECT_ID;
|
|
807
|
+
if (!projectId) {
|
|
808
|
+
throw new Error("NF_PROJECT_ID not set.");
|
|
809
|
+
}
|
|
810
|
+
console.log(`
|
|
811
|
+
\u{1F4A4} Scaling Northflank service to 0: ${instanceId}`);
|
|
812
|
+
try {
|
|
813
|
+
runNfInherit([
|
|
814
|
+
"services",
|
|
815
|
+
"scale",
|
|
816
|
+
"--projectId",
|
|
817
|
+
projectId,
|
|
818
|
+
"--serviceId",
|
|
819
|
+
instanceId,
|
|
820
|
+
"--replicas",
|
|
821
|
+
"0"
|
|
822
|
+
]);
|
|
823
|
+
console.log(" \u2713 Service scaled to 0 (sleeping)");
|
|
824
|
+
} catch (err) {
|
|
825
|
+
throw new Error(`Failed to scale down: ${err.stderr || err.message}`);
|
|
826
|
+
}
|
|
827
|
+
},
|
|
828
|
+
async wake(instanceId) {
|
|
829
|
+
const projectId = process.env.NF_PROJECT_ID;
|
|
830
|
+
if (!projectId) {
|
|
831
|
+
throw new Error("NF_PROJECT_ID not set.");
|
|
832
|
+
}
|
|
833
|
+
console.log(`
|
|
834
|
+
\u2600\uFE0F Scaling Northflank service to 1: ${instanceId}`);
|
|
835
|
+
try {
|
|
836
|
+
runNfInherit([
|
|
837
|
+
"services",
|
|
838
|
+
"scale",
|
|
839
|
+
"--projectId",
|
|
840
|
+
projectId,
|
|
841
|
+
"--serviceId",
|
|
842
|
+
instanceId,
|
|
843
|
+
"--replicas",
|
|
844
|
+
"1"
|
|
845
|
+
]);
|
|
846
|
+
console.log(" \u2713 Service scaling up (may take 30-60s)");
|
|
847
|
+
} catch (err) {
|
|
848
|
+
throw new Error(`Failed to scale up: ${err.stderr || err.message}`);
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
async logs(instanceId, follow = true) {
|
|
852
|
+
const projectId = process.env.NF_PROJECT_ID;
|
|
853
|
+
if (!projectId) {
|
|
854
|
+
throw new Error("NF_PROJECT_ID not set.");
|
|
855
|
+
}
|
|
856
|
+
const args = [
|
|
857
|
+
"services",
|
|
858
|
+
"logs",
|
|
859
|
+
"--projectId",
|
|
860
|
+
projectId,
|
|
861
|
+
"--serviceId",
|
|
862
|
+
instanceId
|
|
863
|
+
];
|
|
864
|
+
if (follow) args.push("--follow");
|
|
865
|
+
const child = spawn2(CLI, args, { stdio: "inherit" });
|
|
866
|
+
return new Promise((resolve3, reject) => {
|
|
867
|
+
child.on("exit", (code) => {
|
|
868
|
+
if (code === 0) resolve3();
|
|
869
|
+
else reject(new Error(`nf logs exited with code ${code}`));
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
},
|
|
873
|
+
async endpoint(instanceId) {
|
|
874
|
+
const projectId = process.env.NF_PROJECT_ID;
|
|
875
|
+
if (!projectId) return `https://${instanceId}.northflank.app`;
|
|
876
|
+
try {
|
|
877
|
+
const info = runNf([
|
|
878
|
+
"services",
|
|
879
|
+
"info",
|
|
880
|
+
"--projectId",
|
|
881
|
+
projectId,
|
|
882
|
+
"--serviceId",
|
|
883
|
+
instanceId
|
|
884
|
+
]);
|
|
885
|
+
const match = info.match(/https?:\/\/[^\s]+/i);
|
|
886
|
+
if (match) return match[0];
|
|
887
|
+
} catch {
|
|
888
|
+
}
|
|
889
|
+
return `https://${instanceId}.${projectId}.northflank.app`;
|
|
890
|
+
},
|
|
891
|
+
async teardown(instanceId) {
|
|
892
|
+
const projectId = process.env.NF_PROJECT_ID;
|
|
893
|
+
if (!projectId) {
|
|
894
|
+
throw new Error("NF_PROJECT_ID not set.");
|
|
895
|
+
}
|
|
896
|
+
console.log(`
|
|
897
|
+
\u{1F5D1}\uFE0F Destroying Northflank service: ${instanceId}`);
|
|
898
|
+
try {
|
|
899
|
+
runNfInherit([
|
|
900
|
+
"services",
|
|
901
|
+
"delete",
|
|
902
|
+
"--projectId",
|
|
903
|
+
projectId,
|
|
904
|
+
"--serviceId",
|
|
905
|
+
instanceId,
|
|
906
|
+
"--permanent"
|
|
907
|
+
]);
|
|
908
|
+
console.log(" \u2713 Service destroyed");
|
|
909
|
+
} catch (err) {
|
|
910
|
+
throw new Error(`Failed to destroy service: ${err.stderr || err.message}`);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
});
|
|
916
|
+
|
|
917
|
+
// src/deploy/providers/daytona.provider.ts
|
|
918
|
+
var daytona_provider_exports = {};
|
|
919
|
+
__export(daytona_provider_exports, {
|
|
920
|
+
daytonaProvider: () => daytonaProvider
|
|
921
|
+
});
|
|
922
|
+
import { execFileSync as execFileSync4, spawn as spawn3 } from "child_process";
|
|
923
|
+
import { existsSync as existsSync4, unlinkSync as unlinkSync4 } from "fs";
|
|
924
|
+
import { tmpdir as tmpdir4 } from "os";
|
|
925
|
+
import { basename as basename4, join as join4 } from "path";
|
|
926
|
+
function runDaytona(args) {
|
|
927
|
+
return execFileSync4(CLI2, args, {
|
|
928
|
+
encoding: "utf-8",
|
|
929
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
function runDaytonaInherit(args) {
|
|
933
|
+
execFileSync4(CLI2, args, { stdio: "inherit" });
|
|
934
|
+
}
|
|
935
|
+
var CLI2, daytonaProvider;
|
|
936
|
+
var init_daytona_provider = __esm({
|
|
937
|
+
"src/deploy/providers/daytona.provider.ts"() {
|
|
938
|
+
"use strict";
|
|
939
|
+
init_esm_shims();
|
|
940
|
+
CLI2 = "daytona";
|
|
941
|
+
daytonaProvider = {
|
|
942
|
+
name: "daytona",
|
|
943
|
+
label: "Daytona.io (Firecracker / Dev Workspace)",
|
|
944
|
+
defaultName(projectDir) {
|
|
945
|
+
return basename4(projectDir).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40);
|
|
946
|
+
},
|
|
947
|
+
async authCheck() {
|
|
948
|
+
try {
|
|
949
|
+
runDaytona(["info"]);
|
|
950
|
+
} catch (err) {
|
|
951
|
+
if (err.code === "ENOENT") {
|
|
952
|
+
throw new Error(
|
|
953
|
+
"`daytona` CLI not found.\n Install: https://www.daytona.io/docs/getting-started/installation/\n Or: curl -NF https://download.daytona.io/daytona/install.sh | sh"
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
throw new Error(`Daytona CLI error: ${err.stderr || err.message}`);
|
|
957
|
+
}
|
|
958
|
+
},
|
|
959
|
+
async provision(name, opts) {
|
|
960
|
+
try {
|
|
961
|
+
const existing = runDaytona(["list"]);
|
|
962
|
+
const lines = existing.split("\n");
|
|
963
|
+
const found = lines.find((l) => l.includes(name));
|
|
964
|
+
if (found) {
|
|
965
|
+
console.log(` \u26A0\uFE0F Workspace already exists: ${name}`);
|
|
966
|
+
return name;
|
|
967
|
+
}
|
|
968
|
+
} catch {
|
|
969
|
+
}
|
|
970
|
+
console.log(`
|
|
971
|
+
\u{1F4E6} Creating Daytona workspace: ${name}`);
|
|
972
|
+
const image = opts?.memory ? "node:20" : "node:20";
|
|
973
|
+
const args = ["create", "--name", name, "--image", image];
|
|
974
|
+
const provider = process.env.DAYTONA_PROVIDER;
|
|
975
|
+
if (provider) {
|
|
976
|
+
args.push("--provider", provider);
|
|
977
|
+
}
|
|
978
|
+
runDaytonaInherit(args);
|
|
979
|
+
console.log(" \u2713 Workspace created");
|
|
980
|
+
return name;
|
|
981
|
+
},
|
|
982
|
+
async deploy(instanceId, distDir) {
|
|
983
|
+
console.log("\n\u{1F4E4} Uploading build artifacts...");
|
|
984
|
+
const tarPath = join4(tmpdir4(), `hybrid-daytona-${Date.now()}.tar.gz`);
|
|
985
|
+
execFileSync4("tar", ["-czf", tarPath, "-C", distDir, "."], {
|
|
986
|
+
stdio: "pipe"
|
|
987
|
+
});
|
|
988
|
+
runDaytonaInherit([
|
|
989
|
+
"code",
|
|
990
|
+
instanceId,
|
|
991
|
+
"--command",
|
|
992
|
+
"mkdir -p /workspace/app"
|
|
993
|
+
]);
|
|
994
|
+
try {
|
|
995
|
+
runDaytonaInherit([
|
|
996
|
+
"cp",
|
|
997
|
+
tarPath,
|
|
998
|
+
`${instanceId}:/workspace/app/hybrid-deploy.tar.gz`
|
|
999
|
+
]);
|
|
1000
|
+
} catch {
|
|
1001
|
+
throw new Error(
|
|
1002
|
+
`daytona cp failed for instance ${instanceId}.
|
|
1003
|
+
Ensure the Daytona CLI supports file copy: daytona cp ${tarPath} ${instanceId}:/workspace/app/hybrid-deploy.tar.gz`
|
|
1004
|
+
);
|
|
1005
|
+
}
|
|
1006
|
+
runDaytonaInherit([
|
|
1007
|
+
"code",
|
|
1008
|
+
instanceId,
|
|
1009
|
+
"--command",
|
|
1010
|
+
"cd /workspace/app && tar -xzf hybrid-deploy.tar.gz && npm install --production"
|
|
1011
|
+
]);
|
|
1012
|
+
if (existsSync4(tarPath)) {
|
|
1013
|
+
try {
|
|
1014
|
+
unlinkSync4(tarPath);
|
|
1015
|
+
} catch {
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
console.log("\n\u{1F527} Starting agent...");
|
|
1019
|
+
runDaytonaInherit([
|
|
1020
|
+
"code",
|
|
1021
|
+
instanceId,
|
|
1022
|
+
"--command",
|
|
1023
|
+
"cd /workspace/app && export NODE_ENV=production AGENT_PORT=8454 && nohup node server/index.cjs > /workspace/app/agent.log 2>&1 &"
|
|
1024
|
+
]);
|
|
1025
|
+
console.log("\n\u23F3 Waiting for agent to start...");
|
|
1026
|
+
let ready = false;
|
|
1027
|
+
for (let i = 0; i < 15; i++) {
|
|
1028
|
+
try {
|
|
1029
|
+
const result = runDaytona([
|
|
1030
|
+
"code",
|
|
1031
|
+
instanceId,
|
|
1032
|
+
"--command",
|
|
1033
|
+
"curl -sf http://localhost:8454/health || echo not-ready"
|
|
1034
|
+
]);
|
|
1035
|
+
if (result && !result.includes("not-ready")) {
|
|
1036
|
+
ready = true;
|
|
1037
|
+
break;
|
|
1038
|
+
}
|
|
1039
|
+
} catch {
|
|
1040
|
+
}
|
|
1041
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
1042
|
+
}
|
|
1043
|
+
if (!ready) {
|
|
1044
|
+
console.log(" \u26A0\uFE0F Agent health check timed out");
|
|
1045
|
+
} else {
|
|
1046
|
+
console.log(" \u2713 Agent running");
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
async status(instanceId) {
|
|
1050
|
+
try {
|
|
1051
|
+
const list = runDaytona(["list"]);
|
|
1052
|
+
const lines = list.split("\n");
|
|
1053
|
+
const found = lines.find((l) => l.includes(instanceId));
|
|
1054
|
+
if (!found) return "stopped";
|
|
1055
|
+
const lower = found.toLowerCase();
|
|
1056
|
+
if (lower.includes("started") || lower.includes("running")) {
|
|
1057
|
+
return "running";
|
|
1058
|
+
}
|
|
1059
|
+
if (lower.includes("stopped")) {
|
|
1060
|
+
return "stopped";
|
|
1061
|
+
}
|
|
1062
|
+
return "unknown";
|
|
1063
|
+
} catch {
|
|
1064
|
+
return "unknown";
|
|
1065
|
+
}
|
|
1066
|
+
},
|
|
1067
|
+
async sleep(instanceId) {
|
|
1068
|
+
console.log(`
|
|
1069
|
+
\u{1F4A4} Stopping Daytona workspace: ${instanceId}`);
|
|
1070
|
+
try {
|
|
1071
|
+
runDaytonaInherit(["stop", instanceId]);
|
|
1072
|
+
console.log(" \u2713 Workspace stopped");
|
|
1073
|
+
} catch (err) {
|
|
1074
|
+
throw new Error(`Failed to stop workspace: ${err.stderr || err.message}`);
|
|
1075
|
+
}
|
|
1076
|
+
},
|
|
1077
|
+
async wake(instanceId) {
|
|
1078
|
+
console.log(`
|
|
1079
|
+
\u2600\uFE0F Starting Daytona workspace: ${instanceId}`);
|
|
1080
|
+
try {
|
|
1081
|
+
runDaytonaInherit(["start", instanceId]);
|
|
1082
|
+
console.log(" \u2713 Workspace starting (~30s)");
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
throw new Error(`Failed to start workspace: ${err.stderr || err.message}`);
|
|
1085
|
+
}
|
|
1086
|
+
},
|
|
1087
|
+
async logs(instanceId, follow = true) {
|
|
1088
|
+
const args = follow ? ["code", instanceId, "--command", "tail -f /workspace/app/agent.log"] : ["code", instanceId, "--command", "cat /workspace/app/agent.log"];
|
|
1089
|
+
const child = spawn3(CLI2, args, { stdio: "inherit" });
|
|
1090
|
+
return new Promise((resolve3, reject) => {
|
|
1091
|
+
child.on("exit", (code) => {
|
|
1092
|
+
if (code === 0) resolve3();
|
|
1093
|
+
else reject(new Error(`daytona exited with code ${code}`));
|
|
1094
|
+
});
|
|
1095
|
+
});
|
|
1096
|
+
},
|
|
1097
|
+
async endpoint(instanceId) {
|
|
1098
|
+
return `daytona://${instanceId}`;
|
|
1099
|
+
},
|
|
1100
|
+
async teardown(instanceId) {
|
|
1101
|
+
console.log(`
|
|
1102
|
+
\u{1F5D1}\uFE0F Destroying Daytona workspace: ${instanceId}`);
|
|
1103
|
+
try {
|
|
1104
|
+
runDaytonaInherit(["remove", instanceId]);
|
|
1105
|
+
console.log(" \u2713 Workspace destroyed");
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
throw new Error(
|
|
1108
|
+
`Failed to destroy workspace: ${err.stderr || err.message}`
|
|
1109
|
+
);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
});
|
|
1115
|
+
|
|
1116
|
+
// src/deploy/index.ts
|
|
1117
|
+
async function getProvider(name) {
|
|
1118
|
+
const entry = providers[name];
|
|
1119
|
+
if (!entry) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`Provider "${name}" is not yet implemented.
|
|
1122
|
+
Available providers: sprites, e2b, northflank, daytona`
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
if (typeof entry === "function") {
|
|
1126
|
+
return entry();
|
|
1127
|
+
}
|
|
1128
|
+
return entry;
|
|
1129
|
+
}
|
|
1130
|
+
var providers;
|
|
1131
|
+
var init_deploy = __esm({
|
|
1132
|
+
"src/deploy/index.ts"() {
|
|
1133
|
+
"use strict";
|
|
1134
|
+
init_esm_shims();
|
|
1135
|
+
providers = {
|
|
1136
|
+
sprites: () => Promise.resolve().then(() => (init_sprite_provider(), sprite_provider_exports)).then((m) => m.spriteProvider),
|
|
1137
|
+
e2b: () => Promise.resolve().then(() => (init_e2b_provider(), e2b_provider_exports)).then((m) => m.e2bProvider),
|
|
1138
|
+
northflank: () => Promise.resolve().then(() => (init_northflank_provider(), northflank_provider_exports)).then((m) => m.northflankProvider),
|
|
1139
|
+
daytona: () => Promise.resolve().then(() => (init_daytona_provider(), daytona_provider_exports)).then((m) => m.daytonaProvider)
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// src/deploy/deploy.ts
|
|
1145
|
+
var deploy_exports = {};
|
|
1146
|
+
__export(deploy_exports, {
|
|
1147
|
+
runDeploy: () => runDeploy,
|
|
1148
|
+
runLogs: () => runLogs,
|
|
1149
|
+
runSleep: () => runSleep,
|
|
1150
|
+
runStatus: () => runStatus,
|
|
1151
|
+
runTeardown: () => runTeardown,
|
|
1152
|
+
runWake: () => runWake
|
|
1153
|
+
});
|
|
1154
|
+
import {
|
|
1155
|
+
cpSync,
|
|
1156
|
+
existsSync as existsSync5,
|
|
1157
|
+
mkdirSync,
|
|
1158
|
+
readFileSync,
|
|
1159
|
+
rmSync,
|
|
1160
|
+
writeFileSync
|
|
1161
|
+
} from "fs";
|
|
1162
|
+
import { resolve } from "path";
|
|
1163
|
+
async function runDeploy(options, projectRoot2, packageDir2) {
|
|
1164
|
+
const platform = resolvePlatform(options.platform, projectRoot2);
|
|
1165
|
+
const provider = await getProvider(platform);
|
|
1166
|
+
console.log(`
|
|
1167
|
+
\u{1F680} Deploying to ${provider.label}...`);
|
|
1168
|
+
try {
|
|
1169
|
+
await provider.authCheck();
|
|
1170
|
+
} catch (err) {
|
|
1171
|
+
console.error(`
|
|
1172
|
+
\u274C ${err.message}`);
|
|
1173
|
+
process.exit(1);
|
|
1174
|
+
}
|
|
1175
|
+
if (!options.skipBuild) {
|
|
1176
|
+
await runBuild(projectRoot2, packageDir2);
|
|
1177
|
+
}
|
|
1178
|
+
const distDir = resolve(projectRoot2, "dist");
|
|
1179
|
+
const instanceName = options.name || provider.defaultName(projectRoot2);
|
|
1180
|
+
console.log(`
|
|
1181
|
+
\u{1F4CB} Instance: ${instanceName}`);
|
|
1182
|
+
const existingStatus = await provider.status(instanceName);
|
|
1183
|
+
if (options.force) {
|
|
1184
|
+
if (existingStatus === "running" || existingStatus === "sleeping") {
|
|
1185
|
+
await provider.teardown(instanceName);
|
|
1186
|
+
}
|
|
1187
|
+
await provider.provision(instanceName);
|
|
1188
|
+
} else if (existingStatus === "sleeping") {
|
|
1189
|
+
console.log(` \u2600\uFE0F Waking sleeping instance...`);
|
|
1190
|
+
await provider.wake(instanceName);
|
|
1191
|
+
} else if (existingStatus === "running") {
|
|
1192
|
+
console.log(` \u26A0\uFE0F Instance "${instanceName}" is already running.`);
|
|
1193
|
+
const prompts = (await import("prompts")).default;
|
|
1194
|
+
const choice = await prompts({
|
|
1195
|
+
type: "select",
|
|
1196
|
+
name: "action",
|
|
1197
|
+
message: "What do you want to do?",
|
|
1198
|
+
choices: [
|
|
1199
|
+
{ title: "Redeploy (overwrite artifacts)", value: "redeploy" },
|
|
1200
|
+
{ title: "Force recreate VM", value: "force" },
|
|
1201
|
+
{ title: "Cancel", value: "cancel" }
|
|
1202
|
+
]
|
|
1203
|
+
});
|
|
1204
|
+
if (choice.action === "cancel") {
|
|
1205
|
+
console.log("\n Cancelled.\n");
|
|
1206
|
+
process.exit(0);
|
|
1207
|
+
}
|
|
1208
|
+
if (choice.action === "force") {
|
|
1209
|
+
await provider.teardown(instanceName);
|
|
1210
|
+
await provider.provision(instanceName);
|
|
1211
|
+
}
|
|
1212
|
+
} else if (existingStatus === "unknown" || existingStatus === "stopped") {
|
|
1213
|
+
await provider.provision(instanceName);
|
|
1214
|
+
} else if (existingStatus === "provisioning") {
|
|
1215
|
+
console.error(` \u23F3 Instance still provisioning, try again shortly.`);
|
|
1216
|
+
process.exit(1);
|
|
1217
|
+
} else if (existingStatus === "error") {
|
|
1218
|
+
console.error(` \u274C Instance in error state. Run 'hybrid deploy teardown ${instanceName}' then retry.`);
|
|
1219
|
+
process.exit(1);
|
|
1220
|
+
}
|
|
1221
|
+
await provider.deploy(instanceName, distDir);
|
|
1222
|
+
const endpoint = await provider.endpoint(instanceName);
|
|
1223
|
+
console.log(`
|
|
1224
|
+
Instance: ${instanceName}`);
|
|
1225
|
+
console.log(` URL: ${endpoint}`);
|
|
1226
|
+
console.log(` Health: ${endpoint}/health`);
|
|
1227
|
+
console.log(` Chat: ${endpoint}/api/chat`);
|
|
1228
|
+
console.log("\n\u2705 Deployed!");
|
|
1229
|
+
}
|
|
1230
|
+
async function runSleep(name, platform, projectRoot2) {
|
|
1231
|
+
const provider = await getProvider(
|
|
1232
|
+
resolvePlatform(platform, projectRoot2)
|
|
1233
|
+
);
|
|
1234
|
+
await provider.authCheck();
|
|
1235
|
+
await provider.sleep(name);
|
|
1236
|
+
}
|
|
1237
|
+
async function runWake(name, platform, projectRoot2) {
|
|
1238
|
+
const provider = await getProvider(
|
|
1239
|
+
resolvePlatform(platform, projectRoot2)
|
|
1240
|
+
);
|
|
1241
|
+
await provider.authCheck();
|
|
1242
|
+
await provider.wake(name);
|
|
1243
|
+
}
|
|
1244
|
+
async function runStatus(name, platform, projectRoot2) {
|
|
1245
|
+
const provider = await getProvider(
|
|
1246
|
+
resolvePlatform(platform, projectRoot2)
|
|
1247
|
+
);
|
|
1248
|
+
await provider.authCheck();
|
|
1249
|
+
const status = await provider.status(name);
|
|
1250
|
+
const endpoint = await provider.endpoint(name);
|
|
1251
|
+
console.log(`
|
|
1252
|
+
\u{1F4CB} Instance: ${name}`);
|
|
1253
|
+
console.log(` Status: ${status}`);
|
|
1254
|
+
console.log(` URL: ${endpoint}`);
|
|
1255
|
+
}
|
|
1256
|
+
async function runLogs(name, follow, platform, projectRoot2) {
|
|
1257
|
+
const provider = await getProvider(
|
|
1258
|
+
resolvePlatform(platform, projectRoot2)
|
|
1259
|
+
);
|
|
1260
|
+
await provider.authCheck();
|
|
1261
|
+
await provider.logs(name, follow);
|
|
1262
|
+
}
|
|
1263
|
+
async function runTeardown(name, platform, projectRoot2) {
|
|
1264
|
+
const provider = await getProvider(
|
|
1265
|
+
resolvePlatform(platform, projectRoot2)
|
|
1266
|
+
);
|
|
1267
|
+
await provider.authCheck();
|
|
1268
|
+
await provider.teardown(name);
|
|
1269
|
+
}
|
|
1270
|
+
function resolvePlatform(platform, projectRoot2) {
|
|
1271
|
+
if (platform) return platform;
|
|
1272
|
+
if (projectRoot2) {
|
|
1273
|
+
const configPath = resolve(projectRoot2, "hybrid.config.ts");
|
|
1274
|
+
if (existsSync5(configPath)) {
|
|
1275
|
+
const content = readFileSync(configPath, "utf-8");
|
|
1276
|
+
const match = content.match(
|
|
1277
|
+
/(?:platform|deployPlatform)\s*[=:]\s*["']([^"']+)["']/
|
|
1278
|
+
);
|
|
1279
|
+
if (match) return match[1];
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return "sprites";
|
|
1283
|
+
}
|
|
1284
|
+
async function runBuild(projectRoot2, packageDir2) {
|
|
1285
|
+
const distDir = resolve(projectRoot2, "dist");
|
|
1286
|
+
console.log("\n\u{1F527} Building agent...");
|
|
1287
|
+
if (existsSync5(distDir)) {
|
|
1288
|
+
rmSync(distDir, { recursive: true, force: true });
|
|
1289
|
+
}
|
|
1290
|
+
mkdirSync(distDir, { recursive: true });
|
|
1291
|
+
mkdirSync(resolve(distDir, "server"), { recursive: true });
|
|
1292
|
+
console.log("\u{1F4E6} Copying agent runtime...");
|
|
1293
|
+
const agentDistDir = resolve(packageDir2, "dist");
|
|
1294
|
+
const files = ["server/index.cjs"];
|
|
1295
|
+
for (const file of files) {
|
|
1296
|
+
const src = resolve(agentDistDir, file);
|
|
1297
|
+
if (existsSync5(src)) {
|
|
1298
|
+
cpSync(src, resolve(distDir, file));
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
console.log("\u{1F4CB} Copying agent config...");
|
|
1302
|
+
const configFiles = [
|
|
1303
|
+
"SOUL.md",
|
|
1304
|
+
"AGENTS.md",
|
|
1305
|
+
"IDENTITY.md",
|
|
1306
|
+
"TOOLS.md",
|
|
1307
|
+
"BOOT.md",
|
|
1308
|
+
"BOOTSTRAP.md",
|
|
1309
|
+
"HEARTBEAT.md",
|
|
1310
|
+
"USER.md"
|
|
1311
|
+
];
|
|
1312
|
+
for (const file of configFiles) {
|
|
1313
|
+
const src = resolve(projectRoot2, file);
|
|
1314
|
+
if (existsSync5(src)) {
|
|
1315
|
+
cpSync(src, resolve(distDir, file));
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1318
|
+
const credsDir = resolve(projectRoot2, "credentials");
|
|
1319
|
+
if (existsSync5(credsDir)) {
|
|
1320
|
+
cpSync(credsDir, resolve(distDir, "credentials"), { recursive: true });
|
|
1321
|
+
}
|
|
1322
|
+
const deployPkg = {
|
|
1323
|
+
name: "hybrid",
|
|
1324
|
+
version: "1.0.0",
|
|
1325
|
+
type: "module",
|
|
1326
|
+
dependencies: {
|
|
1327
|
+
"@anthropic-ai/claude-agent-sdk": "^0.2.38",
|
|
1328
|
+
"@hono/node-server": "^1.13.5",
|
|
1329
|
+
ai: "^6.0.0",
|
|
1330
|
+
"better-sqlite3": "^11.0.0",
|
|
1331
|
+
dotenv: "^16.4.5",
|
|
1332
|
+
hono: "^4.10.8",
|
|
1333
|
+
"sql.js": "^1.11.0",
|
|
1334
|
+
zod: "^4.0.0"
|
|
1335
|
+
}
|
|
1336
|
+
};
|
|
1337
|
+
writeFileSync(
|
|
1338
|
+
resolve(distDir, "package.json"),
|
|
1339
|
+
JSON.stringify(deployPkg, null, 2)
|
|
1340
|
+
);
|
|
1341
|
+
const present = configFiles.filter(
|
|
1342
|
+
(f) => existsSync5(resolve(distDir, f))
|
|
1343
|
+
);
|
|
1344
|
+
const hasCredentials = existsSync5(resolve(distDir, "credentials"));
|
|
1345
|
+
const configCopy = present.length > 0 ? `COPY ${present.join(" ")} ./` : "";
|
|
1346
|
+
const credCopy = hasCredentials ? "COPY credentials/ ./credentials/" : "COPY .hybrid-deploy.json ./";
|
|
1347
|
+
writeFileSync(
|
|
1348
|
+
resolve(distDir, "Dockerfile"),
|
|
1349
|
+
`FROM node:20-bookworm-slim
|
|
1350
|
+
WORKDIR /app
|
|
1351
|
+
COPY package.json ./
|
|
1352
|
+
RUN npm install --production
|
|
1353
|
+
COPY server/ ./server/
|
|
1354
|
+
COPY ${configCopy} ./
|
|
1355
|
+
${credCopy}
|
|
1356
|
+
ENV AGENT_PORT=8454
|
|
1357
|
+
ENV NODE_ENV=production
|
|
1358
|
+
ENV DATA_ROOT=/app/data
|
|
1359
|
+
EXPOSE 8454
|
|
1360
|
+
USER node
|
|
1361
|
+
CMD ["node", "server/index.cjs"]
|
|
1362
|
+
`
|
|
1363
|
+
);
|
|
1364
|
+
writeFileSync(
|
|
1365
|
+
resolve(distDir, "start.sh"),
|
|
1366
|
+
`#!/bin/sh
|
|
1367
|
+
node server/index.cjs
|
|
1368
|
+
`
|
|
1369
|
+
);
|
|
1370
|
+
writeFileSync(
|
|
1371
|
+
resolve(distDir, ".hybrid-deploy.json"),
|
|
1372
|
+
JSON.stringify(
|
|
1373
|
+
{
|
|
1374
|
+
version: 1,
|
|
1375
|
+
provider: "firecracker",
|
|
1376
|
+
startCommand: "node server/index.cjs",
|
|
1377
|
+
port: 8454,
|
|
1378
|
+
healthPath: "/health"
|
|
1379
|
+
},
|
|
1380
|
+
null,
|
|
1381
|
+
2
|
|
1382
|
+
)
|
|
1383
|
+
);
|
|
1384
|
+
console.log("\n\u2705 Build complete!");
|
|
1385
|
+
console.log(` Output: ${distDir}`);
|
|
1386
|
+
}
|
|
1387
|
+
var init_deploy2 = __esm({
|
|
1388
|
+
"src/deploy/deploy.ts"() {
|
|
1389
|
+
"use strict";
|
|
1390
|
+
init_esm_shims();
|
|
1391
|
+
init_deploy();
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
|
|
1395
|
+
// src/cli.ts
|
|
1396
|
+
init_esm_shims();
|
|
1397
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1398
|
+
import { dirname, resolve as resolve2 } from "path";
|
|
1399
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
1400
|
import { config } from "dotenv";
|
|
8
1401
|
function findProjectRoot(startDir) {
|
|
9
1402
|
const markers = ["package.json", "hybrid.config.ts", "SOUL.md"];
|
|
10
1403
|
let current = startDir;
|
|
11
1404
|
while (current !== "/") {
|
|
12
1405
|
for (const marker of markers) {
|
|
13
|
-
if (
|
|
1406
|
+
if (existsSync6(resolve2(current, marker))) {
|
|
14
1407
|
return current;
|
|
15
1408
|
}
|
|
16
1409
|
}
|
|
17
|
-
const parent =
|
|
1410
|
+
const parent = resolve2(current, "..");
|
|
18
1411
|
if (parent === current) break;
|
|
19
1412
|
current = parent;
|
|
20
1413
|
}
|
|
@@ -27,9 +1420,9 @@ if (cwdIndex !== -1 && process.argv[cwdIndex + 1]) {
|
|
|
27
1420
|
}
|
|
28
1421
|
var projectRoot = findProjectRoot(process.cwd());
|
|
29
1422
|
for (const envFile of [".env", ".env.local"]) {
|
|
30
|
-
const
|
|
31
|
-
if (
|
|
32
|
-
config({ path });
|
|
1423
|
+
const path2 = resolve2(projectRoot, envFile);
|
|
1424
|
+
if (existsSync6(path2)) {
|
|
1425
|
+
config({ path: path2 });
|
|
33
1426
|
}
|
|
34
1427
|
}
|
|
35
1428
|
var [major] = process.versions.node.split(".").map(Number);
|
|
@@ -37,20 +1430,19 @@ if (!major || major < 20) {
|
|
|
37
1430
|
console.error("Error: Node.js version 20 or higher is required");
|
|
38
1431
|
process.exit(1);
|
|
39
1432
|
}
|
|
40
|
-
var
|
|
41
|
-
var packageDir =
|
|
1433
|
+
var __dirname2 = dirname(fileURLToPath2(import.meta.url));
|
|
1434
|
+
var packageDir = resolve2(__dirname2, "..");
|
|
42
1435
|
async function main() {
|
|
43
1436
|
const args = process.argv.slice(2);
|
|
44
1437
|
const command = args[0];
|
|
45
|
-
if (command === "build")
|
|
1438
|
+
if (command === "build") {
|
|
1439
|
+
const targetFlag = args.indexOf("--target");
|
|
1440
|
+
return build(targetFlag !== -1 ? args[targetFlag + 1] : args[1]);
|
|
1441
|
+
}
|
|
46
1442
|
if (command === "dev") return dev(args.includes("--docker"));
|
|
47
1443
|
if (command === "start") return start();
|
|
48
|
-
if (command === "deploy") return
|
|
1444
|
+
if (command === "deploy") return deployCommand(args);
|
|
49
1445
|
if (command === "init") return init(args[1]);
|
|
50
|
-
if (command === "keygen") return keygen(args[1]);
|
|
51
|
-
if (command === "register") return register();
|
|
52
|
-
if (command === "revoke") return revoke(args[1]);
|
|
53
|
-
if (command === "revoke-all") return revokeAll();
|
|
54
1446
|
if (command === "owner") {
|
|
55
1447
|
const subcommand = args[1];
|
|
56
1448
|
if (subcommand === "add") return ownerAdd(args[2]);
|
|
@@ -109,17 +1501,24 @@ async function main() {
|
|
|
109
1501
|
console.log("Usage: hybrid <command>");
|
|
110
1502
|
console.log("");
|
|
111
1503
|
console.log("Commands:");
|
|
112
|
-
console.log(" init <name>
|
|
113
|
-
console.log(" dev
|
|
114
|
-
console.log(" build [--target]
|
|
115
|
-
console.log(" start
|
|
116
|
-
console.log(
|
|
1504
|
+
console.log(" init <name> Initialize a new agent");
|
|
1505
|
+
console.log(" dev Start development server");
|
|
1506
|
+
console.log(" build [--target] Build for deployment (firecracker)");
|
|
1507
|
+
console.log(" start Run built agent");
|
|
1508
|
+
console.log(
|
|
1509
|
+
" deploy [platform] Deploy to a Firecracker provider"
|
|
1510
|
+
);
|
|
1511
|
+
console.log(" deploy sleep <name> Put VM to sleep");
|
|
1512
|
+
console.log(" deploy wake <name> Wake VM");
|
|
1513
|
+
console.log(" deploy status <name> Show VM status");
|
|
1514
|
+
console.log(" deploy logs <name> Stream agent logs");
|
|
1515
|
+
console.log(" deploy teardown <name> [--all] Destroy VM");
|
|
117
1516
|
console.log("");
|
|
118
|
-
console.log("
|
|
119
|
-
console.log("
|
|
120
|
-
console.log("
|
|
121
|
-
console.log("
|
|
122
|
-
console.log("
|
|
1517
|
+
console.log("Deploy flags:");
|
|
1518
|
+
console.log(" --provider <name> Override provider (sprites, e2b, northflank, daytona)");
|
|
1519
|
+
console.log(" --name <name> Override instance name");
|
|
1520
|
+
console.log(" --force Recreate VM even if it exists");
|
|
1521
|
+
console.log(" --no-build Skip build step");
|
|
123
1522
|
console.log("");
|
|
124
1523
|
console.log("Owner:");
|
|
125
1524
|
console.log(" owner add <address> Add an owner");
|
|
@@ -225,36 +1624,35 @@ async function init(name) {
|
|
|
225
1624
|
process.exit(1);
|
|
226
1625
|
}
|
|
227
1626
|
const {
|
|
228
|
-
cpSync,
|
|
229
|
-
existsSync:
|
|
230
|
-
mkdirSync,
|
|
231
|
-
writeFileSync,
|
|
1627
|
+
cpSync: cpSync2,
|
|
1628
|
+
existsSync: existsSync7,
|
|
1629
|
+
mkdirSync: mkdirSync2,
|
|
1630
|
+
writeFileSync: writeFileSync2,
|
|
232
1631
|
readdirSync,
|
|
233
|
-
readFileSync
|
|
1632
|
+
readFileSync: readFileSync2
|
|
234
1633
|
} = await import("fs");
|
|
235
|
-
const
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
if (existsSync2(targetDir)) {
|
|
1634
|
+
const templateDir = resolve2(packageDir, "templates", "agent");
|
|
1635
|
+
const targetDir = resolve2(process.cwd(), name);
|
|
1636
|
+
if (existsSync7(targetDir)) {
|
|
239
1637
|
console.error(`Error: Directory '${name}' already exists`);
|
|
240
1638
|
process.exit(1);
|
|
241
1639
|
}
|
|
242
1640
|
console.log(`
|
|
243
1641
|
\u{1F4E6} Creating agent: ${name}
|
|
244
1642
|
`);
|
|
245
|
-
|
|
246
|
-
const pkgPath =
|
|
247
|
-
const pkg = JSON.parse(
|
|
1643
|
+
cpSync2(templateDir, targetDir, { recursive: true });
|
|
1644
|
+
const pkgPath = resolve2(targetDir, "package.json");
|
|
1645
|
+
const pkg = JSON.parse(readFileSync2(pkgPath, "utf-8"));
|
|
248
1646
|
pkg.name = name;
|
|
249
|
-
|
|
250
|
-
const skillsDir =
|
|
251
|
-
const targetSkillsDir =
|
|
252
|
-
if (
|
|
1647
|
+
writeFileSync2(pkgPath, JSON.stringify(pkg, null, 2));
|
|
1648
|
+
const skillsDir = resolve2(packageDir, "skills");
|
|
1649
|
+
const targetSkillsDir = resolve2(targetDir, "skills");
|
|
1650
|
+
if (existsSync7(skillsDir)) {
|
|
253
1651
|
console.log("\u{1F4DA} Copying core skills...");
|
|
254
1652
|
const coreSkills = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
255
|
-
|
|
1653
|
+
mkdirSync2(targetSkillsDir, { recursive: true });
|
|
256
1654
|
for (const skill of coreSkills) {
|
|
257
|
-
|
|
1655
|
+
cpSync2(resolve2(skillsDir, skill), resolve2(targetSkillsDir, skill), {
|
|
258
1656
|
recursive: true
|
|
259
1657
|
});
|
|
260
1658
|
console.log(` \u2713 ${skill}`);
|
|
@@ -264,36 +1662,16 @@ async function init(name) {
|
|
|
264
1662
|
for (const skill of coreSkills) {
|
|
265
1663
|
lockfile[skill] = { source: "core", installedAt: now };
|
|
266
1664
|
}
|
|
267
|
-
|
|
268
|
-
|
|
1665
|
+
writeFileSync2(
|
|
1666
|
+
resolve2(targetDir, "skills-lock.json"),
|
|
269
1667
|
JSON.stringify(lockfile, null, 2)
|
|
270
1668
|
);
|
|
271
1669
|
}
|
|
272
|
-
const envExamplePath =
|
|
273
|
-
const envPath =
|
|
274
|
-
if (
|
|
275
|
-
|
|
1670
|
+
const envExamplePath = resolve2(targetDir, ".env.example");
|
|
1671
|
+
const envPath = resolve2(targetDir, ".env");
|
|
1672
|
+
if (existsSync7(envExamplePath)) {
|
|
1673
|
+
cpSync2(envExamplePath, envPath);
|
|
276
1674
|
}
|
|
277
|
-
const rl = createInterface({
|
|
278
|
-
input: process.stdin,
|
|
279
|
-
output: process.stdout
|
|
280
|
-
});
|
|
281
|
-
const question = (prompt) => {
|
|
282
|
-
return new Promise((resolve2) => {
|
|
283
|
-
rl.question(prompt, (answer) => {
|
|
284
|
-
resolve2(answer.trim());
|
|
285
|
-
});
|
|
286
|
-
});
|
|
287
|
-
};
|
|
288
|
-
const ownerAddress = await question("\nEnter your wallet address (owner): ");
|
|
289
|
-
console.log("\n\u{1F511} Generating wallet key...");
|
|
290
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
291
|
-
const { randomBytes } = await import("crypto");
|
|
292
|
-
const walletKey = `0x${randomBytes(32).toString("hex")}`;
|
|
293
|
-
const account = privateKeyToAccount(walletKey);
|
|
294
|
-
console.log(` Address: ${account.address}`);
|
|
295
|
-
console.log(` Key: ${walletKey.slice(0, 10)}...${walletKey.slice(-8)}`);
|
|
296
|
-
rl.close();
|
|
297
1675
|
const prompts = (await import("prompts")).default;
|
|
298
1676
|
const providerResponse = await prompts({
|
|
299
1677
|
type: "select",
|
|
@@ -326,22 +1704,28 @@ async function init(name) {
|
|
|
326
1704
|
});
|
|
327
1705
|
openrouterKey = keyResponse.key || "";
|
|
328
1706
|
}
|
|
1707
|
+
const credentialsDir = resolve2(targetDir, "credentials");
|
|
1708
|
+
mkdirSync2(credentialsDir, { recursive: true });
|
|
1709
|
+
const acl = {
|
|
1710
|
+
version: 1,
|
|
1711
|
+
allowFrom: []
|
|
1712
|
+
};
|
|
1713
|
+
const ownerResponse = await prompts({
|
|
1714
|
+
type: "text",
|
|
1715
|
+
name: "address",
|
|
1716
|
+
message: "Enter your wallet address (owner, optional)"
|
|
1717
|
+
});
|
|
1718
|
+
const ownerAddress = ownerResponse?.address?.trim() || "";
|
|
329
1719
|
if (ownerAddress) {
|
|
330
|
-
|
|
331
|
-
const credentialsDir = resolve(targetDir, "credentials");
|
|
332
|
-
mkdirSync(credentialsDir, { recursive: true });
|
|
333
|
-
writeFileSync(
|
|
334
|
-
resolve(credentialsDir, "xmtp-allowFrom.json"),
|
|
335
|
-
JSON.stringify({ version: 1, allowFrom: [normalized] }, null, 2)
|
|
336
|
-
);
|
|
1720
|
+
acl.allowFrom.push(ownerAddress.toLowerCase());
|
|
337
1721
|
console.log(`
|
|
338
|
-
\u2705 Added owner: ${
|
|
1722
|
+
\u2705 Added owner: ${ownerAddress.toLowerCase()}`);
|
|
339
1723
|
}
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
`AGENT_WALLET_KEY=${walletKey}`
|
|
1724
|
+
writeFileSync2(
|
|
1725
|
+
resolve2(credentialsDir, "allowFrom.json"),
|
|
1726
|
+
JSON.stringify(acl, null, 2)
|
|
344
1727
|
);
|
|
1728
|
+
let envContent = readFileSync2(envPath, "utf-8");
|
|
345
1729
|
if (anthropicKey) {
|
|
346
1730
|
envContent = envContent.replace(
|
|
347
1731
|
/ANTHROPIC_API_KEY=.*/,
|
|
@@ -370,7 +1754,7 @@ async function init(name) {
|
|
|
370
1754
|
(match) => `# ${match}`
|
|
371
1755
|
);
|
|
372
1756
|
}
|
|
373
|
-
|
|
1757
|
+
writeFileSync2(envPath, envContent);
|
|
374
1758
|
console.log("\u2705 Updated .env file");
|
|
375
1759
|
console.log(`
|
|
376
1760
|
\u2705 Created agent at: ${name}/`);
|
|
@@ -382,30 +1766,30 @@ async function init(name) {
|
|
|
382
1766
|
async function build(target) {
|
|
383
1767
|
const { execSync } = await import("child_process");
|
|
384
1768
|
const {
|
|
385
|
-
cpSync,
|
|
386
|
-
existsSync:
|
|
387
|
-
mkdirSync,
|
|
388
|
-
rmSync,
|
|
389
|
-
writeFileSync,
|
|
1769
|
+
cpSync: cpSync2,
|
|
1770
|
+
existsSync: existsSync7,
|
|
1771
|
+
mkdirSync: mkdirSync2,
|
|
1772
|
+
rmSync: rmSync2,
|
|
1773
|
+
writeFileSync: writeFileSync2,
|
|
390
1774
|
readdirSync,
|
|
391
|
-
readFileSync
|
|
1775
|
+
readFileSync: readFileSync2
|
|
392
1776
|
} = await import("fs");
|
|
393
1777
|
const projectDir = projectRoot;
|
|
394
|
-
const distDir =
|
|
395
|
-
const buildTarget = target || "
|
|
1778
|
+
const distDir = resolve2(projectDir, "dist");
|
|
1779
|
+
const buildTarget = target || "firecracker";
|
|
396
1780
|
console.log("\n\u{1F527} Building agent...");
|
|
397
|
-
if (
|
|
398
|
-
|
|
1781
|
+
if (existsSync7(distDir)) {
|
|
1782
|
+
rmSync2(distDir, { recursive: true, force: true });
|
|
399
1783
|
}
|
|
400
|
-
|
|
401
|
-
|
|
1784
|
+
mkdirSync2(distDir, { recursive: true });
|
|
1785
|
+
mkdirSync2(resolve2(distDir, "server"), { recursive: true });
|
|
402
1786
|
console.log("\u{1F4E6} Copying agent runtime...");
|
|
403
|
-
const agentDistDir =
|
|
404
|
-
const files = ["server/index.cjs"
|
|
1787
|
+
const agentDistDir = resolve2(packageDir, "dist");
|
|
1788
|
+
const files = ["server/index.cjs"];
|
|
405
1789
|
for (const file of files) {
|
|
406
|
-
const src =
|
|
407
|
-
if (
|
|
408
|
-
|
|
1790
|
+
const src = resolve2(agentDistDir, file);
|
|
1791
|
+
if (existsSync7(src)) {
|
|
1792
|
+
cpSync2(src, resolve2(distDir, file));
|
|
409
1793
|
} else {
|
|
410
1794
|
console.error(` Missing: ${file} - run 'pnpm build' in hybrid package`);
|
|
411
1795
|
}
|
|
@@ -421,41 +1805,41 @@ async function build(target) {
|
|
|
421
1805
|
"HEARTBEAT.md",
|
|
422
1806
|
"USER.md"
|
|
423
1807
|
]) {
|
|
424
|
-
const src =
|
|
425
|
-
if (
|
|
426
|
-
|
|
1808
|
+
const src = resolve2(projectDir, file);
|
|
1809
|
+
if (existsSync7(src)) {
|
|
1810
|
+
cpSync2(src, resolve2(distDir, file));
|
|
427
1811
|
console.log(` \u2713 ${file}`);
|
|
428
1812
|
}
|
|
429
1813
|
}
|
|
430
|
-
const hybridConfig =
|
|
431
|
-
const openclawConfig =
|
|
432
|
-
const agentConfig =
|
|
433
|
-
if (
|
|
434
|
-
|
|
1814
|
+
const hybridConfig = resolve2(projectDir, "hybrid.config.ts");
|
|
1815
|
+
const openclawConfig = resolve2(projectDir, "openclaw.json");
|
|
1816
|
+
const agentConfig = resolve2(projectDir, "agent.ts");
|
|
1817
|
+
if (existsSync7(hybridConfig)) {
|
|
1818
|
+
cpSync2(hybridConfig, resolve2(distDir, "hybrid.config.ts"));
|
|
435
1819
|
console.log(" \u2713 hybrid.config.ts");
|
|
436
|
-
} else if (
|
|
437
|
-
const content =
|
|
438
|
-
|
|
439
|
-
|
|
1820
|
+
} else if (existsSync7(openclawConfig)) {
|
|
1821
|
+
const content = readFileSync2(openclawConfig, "utf-8");
|
|
1822
|
+
writeFileSync2(
|
|
1823
|
+
resolve2(projectDir, "hybrid.config.ts"),
|
|
440
1824
|
`// Migrated from openclaw.json
|
|
441
1825
|
export default ${content}`
|
|
442
1826
|
);
|
|
443
1827
|
console.log(" \u2713 Migrated openclaw.json \u2192 hybrid.config.ts");
|
|
444
|
-
} else if (
|
|
445
|
-
|
|
1828
|
+
} else if (existsSync7(agentConfig)) {
|
|
1829
|
+
cpSync2(agentConfig, resolve2(distDir, "agent.ts"));
|
|
446
1830
|
console.log(" \u2713 agent.ts (legacy)");
|
|
447
1831
|
}
|
|
448
|
-
const skillsDir =
|
|
449
|
-
if (
|
|
1832
|
+
const skillsDir = resolve2(projectDir, "skills");
|
|
1833
|
+
if (existsSync7(skillsDir)) {
|
|
450
1834
|
const skills = readdirSync(skillsDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name);
|
|
451
1835
|
console.log(`\u{1F4DA} Skills: ${skills.length} in ./skills/`);
|
|
452
1836
|
} else {
|
|
453
1837
|
console.log("\u26A0\uFE0F No ./skills/ directory found - run 'hybrid init' first?");
|
|
454
1838
|
}
|
|
455
|
-
const credsDir =
|
|
456
|
-
if (
|
|
457
|
-
|
|
458
|
-
|
|
1839
|
+
const credsDir = resolve2(projectDir, "credentials");
|
|
1840
|
+
if (existsSync7(credsDir)) {
|
|
1841
|
+
mkdirSync2(resolve2(distDir, "credentials"), { recursive: true });
|
|
1842
|
+
cpSync2(credsDir, resolve2(distDir, "credentials"), { recursive: true });
|
|
459
1843
|
console.log(" \u2713 credentials/");
|
|
460
1844
|
}
|
|
461
1845
|
const deployPkg = {
|
|
@@ -465,215 +1849,113 @@ export default ${content}`
|
|
|
465
1849
|
dependencies: {
|
|
466
1850
|
"@anthropic-ai/claude-agent-sdk": "^0.2.38",
|
|
467
1851
|
"@hono/node-server": "^1.13.5",
|
|
468
|
-
"@xmtp/agent-sdk": "0.0.14",
|
|
469
|
-
"@xmtp/node-bindings": "^1.9.1",
|
|
470
|
-
"@xmtp/node-sdk": "^4.1.0",
|
|
471
1852
|
ai: "^6.0.0",
|
|
472
1853
|
"better-sqlite3": "^11.0.0",
|
|
473
1854
|
dotenv: "^16.4.5",
|
|
474
1855
|
hono: "^4.10.8",
|
|
475
1856
|
"sql.js": "^1.11.0",
|
|
476
|
-
viem: "^2.46.2",
|
|
477
1857
|
zod: "^4.0.0"
|
|
478
1858
|
}
|
|
479
1859
|
};
|
|
480
|
-
|
|
481
|
-
|
|
1860
|
+
writeFileSync2(
|
|
1861
|
+
resolve2(distDir, "package.json"),
|
|
482
1862
|
JSON.stringify(deployPkg, null, 2)
|
|
483
1863
|
);
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
1864
|
+
writeFileSync2(resolve2(distDir, "Dockerfile"), generateDockerfile(distDir));
|
|
1865
|
+
writeFileSync2(
|
|
1866
|
+
resolve2(distDir, ".hybrid-deploy.json"),
|
|
1867
|
+
JSON.stringify(
|
|
1868
|
+
{
|
|
1869
|
+
version: 1,
|
|
1870
|
+
provider: "firecracker",
|
|
1871
|
+
startCommand: "node server/index.cjs",
|
|
1872
|
+
port: 8454,
|
|
1873
|
+
healthPath: "/health"
|
|
1874
|
+
},
|
|
1875
|
+
null,
|
|
1876
|
+
2
|
|
1877
|
+
)
|
|
1878
|
+
);
|
|
1879
|
+
writeFileSync2(
|
|
1880
|
+
resolve2(distDir, "start.sh"),
|
|
495
1881
|
`#!/bin/sh
|
|
496
|
-
node server/index.cjs
|
|
497
|
-
node xmtp.cjs &
|
|
498
|
-
wait
|
|
1882
|
+
node server/index.cjs
|
|
499
1883
|
`
|
|
500
1884
|
);
|
|
501
1885
|
console.log("\n\u2705 Build complete!");
|
|
502
1886
|
console.log(` Output: ${distDir}`);
|
|
503
1887
|
console.log(` Target: ${buildTarget}`);
|
|
504
1888
|
}
|
|
505
|
-
function generateDockerfile(
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
1889
|
+
function generateDockerfile(distDir) {
|
|
1890
|
+
const configFiles = [
|
|
1891
|
+
"SOUL.md",
|
|
1892
|
+
"AGENTS.md",
|
|
1893
|
+
"IDENTITY.md",
|
|
1894
|
+
"TOOLS.md",
|
|
1895
|
+
"BOOT.md",
|
|
1896
|
+
"BOOTSTRAP.md",
|
|
1897
|
+
"HEARTBEAT.md",
|
|
1898
|
+
"USER.md"
|
|
1899
|
+
];
|
|
1900
|
+
const present = configFiles.filter(
|
|
1901
|
+
(f) => existsSync6(resolve2(distDir, f))
|
|
1902
|
+
);
|
|
1903
|
+
const hasCredentials = existsSync6(resolve2(distDir, "credentials"));
|
|
1904
|
+
const configCopy = present.length > 0 ? `COPY ${present.join(" ")} ./` : "";
|
|
1905
|
+
const credCopy = hasCredentials ? "COPY credentials/ ./credentials/" : "";
|
|
1906
|
+
return `FROM node:20-bookworm-slim
|
|
509
1907
|
WORKDIR /app
|
|
510
|
-
|
|
511
|
-
# Copy built agent runtime
|
|
512
|
-
COPY server/ ./server/
|
|
513
|
-
COPY xmtp.cjs ./
|
|
514
|
-
|
|
515
|
-
# Copy config
|
|
516
|
-
COPY SOUL.md ./
|
|
517
|
-
COPY AGENTS.md ./
|
|
518
|
-
COPY IDENTITY.md ./
|
|
519
|
-
COPY TOOLS.md ./
|
|
520
|
-
COPY BOOTSTRAP.md ./
|
|
521
|
-
COPY HEARTBEAT.md ./
|
|
522
|
-
COPY USER.md ./
|
|
523
|
-
|
|
524
|
-
# Copy credentials (owner ACL)
|
|
525
|
-
COPY credentials/ ./credentials/
|
|
526
|
-
|
|
527
|
-
# Copy deployment files
|
|
528
1908
|
COPY package.json ./
|
|
529
|
-
COPY start.sh ./
|
|
530
|
-
|
|
531
|
-
# Install dependencies
|
|
532
1909
|
RUN npm install --production
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
chown -R node:node /app
|
|
537
|
-
|
|
1910
|
+
COPY server/ ./server/
|
|
1911
|
+
${configCopy}
|
|
1912
|
+
${credCopy}
|
|
538
1913
|
ENV AGENT_PORT=8454
|
|
539
1914
|
ENV NODE_ENV=production
|
|
1915
|
+
ENV DATA_ROOT=/app/data
|
|
540
1916
|
EXPOSE 8454
|
|
541
|
-
|
|
542
1917
|
USER node
|
|
543
|
-
CMD ["
|
|
544
|
-
`;
|
|
545
|
-
}
|
|
546
|
-
return `FROM node:20
|
|
547
|
-
WORKDIR /app
|
|
548
|
-
COPY . ./
|
|
549
|
-
RUN npm install --production
|
|
550
|
-
RUN chown -R node:node /app
|
|
551
|
-
USER node
|
|
552
|
-
CMD ["sh", "start.sh"]
|
|
553
|
-
`;
|
|
554
|
-
}
|
|
555
|
-
function generateFlyToml(appName = "hybrid-agent") {
|
|
556
|
-
return `# Generated by hybrid build
|
|
557
|
-
app = "${appName}"
|
|
558
|
-
primary_region = "iad"
|
|
559
|
-
|
|
560
|
-
[build]
|
|
561
|
-
dockerfile = "Dockerfile"
|
|
562
|
-
context = "."
|
|
563
|
-
|
|
564
|
-
[deployment]
|
|
565
|
-
min_machines = 1
|
|
566
|
-
max_machines = 1
|
|
567
|
-
|
|
568
|
-
[env]
|
|
569
|
-
XMTP_ENV = "production"
|
|
570
|
-
|
|
571
|
-
[[services]]
|
|
572
|
-
protocol = "tcp"
|
|
573
|
-
internal_port = 8454
|
|
574
|
-
|
|
575
|
-
[[services.ports]]
|
|
576
|
-
port = 80
|
|
577
|
-
handlers = ["http"]
|
|
578
|
-
force_https = true
|
|
579
|
-
|
|
580
|
-
[[services.ports]]
|
|
581
|
-
port = 443
|
|
582
|
-
handlers = ["tls", "http"]
|
|
583
|
-
|
|
584
|
-
[vm]
|
|
585
|
-
memory = "1gb"
|
|
586
|
-
cpu_kind = "shared"
|
|
587
|
-
cpus = 1
|
|
1918
|
+
CMD ["node", "server/index.cjs"]
|
|
588
1919
|
`;
|
|
589
1920
|
}
|
|
590
1921
|
async function dev(useDocker) {
|
|
591
1922
|
const { execSync } = await import("child_process");
|
|
592
|
-
const { existsSync: existsSync2, readFileSync } = await import("fs");
|
|
593
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
594
1923
|
if (useDocker) {
|
|
595
1924
|
console.log("\n\u{1F433} Docker dev not yet implemented for new structure");
|
|
596
1925
|
console.log("Use 'hybrid dev' without --docker for now");
|
|
597
1926
|
return;
|
|
598
1927
|
}
|
|
599
1928
|
const projectDir = projectRoot;
|
|
600
|
-
const
|
|
601
|
-
if (walletKey) {
|
|
602
|
-
const walletKeyFormatted = walletKey.startsWith("0x") ? walletKey : `0x${walletKey}`;
|
|
603
|
-
const account = privateKeyToAccount(walletKeyFormatted);
|
|
604
|
-
console.log(`
|
|
605
|
-
\u{1F50D} Checking XMTP registration for ${account.address}...`);
|
|
606
|
-
try {
|
|
607
|
-
const result = execSync(
|
|
608
|
-
`npx tsx -e "
|
|
609
|
-
import { createSigner, createXMTPClient } from './src/client';
|
|
610
|
-
const signer = createSigner('${walletKeyFormatted}');
|
|
611
|
-
const client = await createXMTPClient(signer, { env: 'production' });
|
|
612
|
-
console.log('REGISTERED:', client.inboxId);
|
|
613
|
-
process.exit(0);
|
|
614
|
-
"`,
|
|
615
|
-
{
|
|
616
|
-
cwd: resolve(packageDir, "..", "xmtp"),
|
|
617
|
-
encoding: "utf-8",
|
|
618
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
619
|
-
}
|
|
620
|
-
);
|
|
621
|
-
if (result.includes("REGISTERED:")) {
|
|
622
|
-
const inboxId = result.match(/REGISTERED: (.+)/)?.[1];
|
|
623
|
-
console.log(` \u2705 Already registered (inbox: ${inboxId})`);
|
|
624
|
-
}
|
|
625
|
-
} catch (err) {
|
|
626
|
-
const output = err.stdout || err.stderr || "";
|
|
627
|
-
if (output.includes("not registered") || output.includes("No inbox found") || output.includes("incomplete identity") || err.exitCode) {
|
|
628
|
-
console.log(" \u274C Not registered");
|
|
629
|
-
console.log("\n\u{1F4DD} Registering agent on XMTP...");
|
|
630
|
-
execSync("npx pnpm --filter @hybrd/xmtp register", {
|
|
631
|
-
cwd: resolve(packageDir, "..", ".."),
|
|
632
|
-
stdio: "inherit",
|
|
633
|
-
env: { ...process.env, AGENT_WALLET_KEY: walletKey }
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
const agentServer = resolve(packageDir, "dist", "server", "index.cjs");
|
|
639
|
-
const agentXmtp = resolve(packageDir, "dist", "xmtp.cjs");
|
|
1929
|
+
const agentServer = resolve2(packageDir, "dist", "server", "index.cjs");
|
|
640
1930
|
console.log("\n\u{1F680} Starting development server...\n");
|
|
641
1931
|
console.log(` Project: ${projectDir}`);
|
|
642
1932
|
console.log(` Runtime: ${packageDir}
|
|
643
1933
|
`);
|
|
644
1934
|
try {
|
|
645
|
-
execSync(
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
...process.env,
|
|
652
|
-
AGENT_PROJECT_ROOT: projectDir
|
|
653
|
-
}
|
|
1935
|
+
execSync(`node ${agentServer}`, {
|
|
1936
|
+
cwd: projectDir,
|
|
1937
|
+
stdio: "inherit",
|
|
1938
|
+
env: {
|
|
1939
|
+
...process.env,
|
|
1940
|
+
AGENT_PROJECT_ROOT: projectDir
|
|
654
1941
|
}
|
|
655
|
-
);
|
|
1942
|
+
});
|
|
656
1943
|
} catch {
|
|
657
1944
|
console.error("\n\u274C Failed to start dev server");
|
|
658
1945
|
process.exit(1);
|
|
659
1946
|
}
|
|
660
1947
|
}
|
|
661
1948
|
async function start() {
|
|
662
|
-
const { spawn } = await import("child_process");
|
|
663
|
-
const { existsSync:
|
|
1949
|
+
const { spawn: spawn4 } = await import("child_process");
|
|
1950
|
+
const { existsSync: existsSync7 } = await import("fs");
|
|
664
1951
|
const projectDir = projectRoot;
|
|
665
|
-
const distDir =
|
|
666
|
-
if (!
|
|
1952
|
+
const distDir = resolve2(projectDir, "dist");
|
|
1953
|
+
if (!existsSync7(resolve2(distDir, "server", "simple.cjs"))) {
|
|
667
1954
|
console.error("Error: No build found. Run 'hybrid build' first.");
|
|
668
1955
|
process.exit(1);
|
|
669
1956
|
}
|
|
670
1957
|
console.log("\n\u{1F680} Starting agent from ./dist/...\n");
|
|
671
|
-
const server =
|
|
672
|
-
cwd: projectDir,
|
|
673
|
-
stdio: "inherit",
|
|
674
|
-
env: { ...process.env, AGENT_PROJECT_ROOT: projectDir }
|
|
675
|
-
});
|
|
676
|
-
const xmtp = spawn("node", [resolve(distDir, "xmtp.cjs")], {
|
|
1958
|
+
const server = spawn4("node", [resolve2(distDir, "server", "simple.cjs")], {
|
|
677
1959
|
cwd: projectDir,
|
|
678
1960
|
stdio: "inherit",
|
|
679
1961
|
env: { ...process.env, AGENT_PROJECT_ROOT: projectDir }
|
|
@@ -682,395 +1964,188 @@ async function start() {
|
|
|
682
1964
|
if (code !== 0 && code !== null) process.exit(code);
|
|
683
1965
|
};
|
|
684
1966
|
server.on("exit", exitHandler);
|
|
685
|
-
xmtp.on("exit", exitHandler);
|
|
686
1967
|
process.on("SIGINT", () => {
|
|
687
1968
|
server.kill("SIGINT");
|
|
688
|
-
xmtp.kill("SIGINT");
|
|
689
1969
|
process.exit(0);
|
|
690
1970
|
});
|
|
691
1971
|
}
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
697
|
-
const { randomBytes } = await import("crypto");
|
|
698
|
-
const projectDir = projectRoot;
|
|
699
|
-
const distDir = resolve(projectDir, "dist");
|
|
700
|
-
let deployPlatform = platform;
|
|
701
|
-
if (!deployPlatform) {
|
|
702
|
-
const hybridConfig = resolve(projectDir, "hybrid.config.ts");
|
|
703
|
-
if (existsSync2(hybridConfig)) {
|
|
704
|
-
const configContent = readFileSync(hybridConfig, "utf-8");
|
|
705
|
-
const match = configContent.match(
|
|
706
|
-
/deployPlatform\s*[=:]\s*["']([^"']+)["']/
|
|
707
|
-
);
|
|
708
|
-
if (match) {
|
|
709
|
-
deployPlatform = match[1];
|
|
710
|
-
console.log(` Using saved platform: ${deployPlatform}`);
|
|
711
|
-
}
|
|
712
|
-
}
|
|
1972
|
+
var deployModule = null;
|
|
1973
|
+
async function loadDeploy() {
|
|
1974
|
+
if (!deployModule) {
|
|
1975
|
+
deployModule = await Promise.resolve().then(() => (init_deploy2(), deploy_exports));
|
|
713
1976
|
}
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
if (
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
}
|
|
729
|
-
deployPlatform = choice.platform;
|
|
730
|
-
const hybridConfig = resolve(projectDir, "hybrid.config.ts");
|
|
731
|
-
let configContent = "";
|
|
732
|
-
if (existsSync2(hybridConfig)) {
|
|
733
|
-
configContent = readFileSync(hybridConfig, "utf-8");
|
|
734
|
-
}
|
|
735
|
-
if (!configContent.includes("deployPlatform")) {
|
|
736
|
-
const newContent = configContent ? configContent.replace(
|
|
737
|
-
/export default/,
|
|
738
|
-
`const deployPlatform = "${deployPlatform}"
|
|
739
|
-
|
|
740
|
-
export default`
|
|
741
|
-
) : `const deployPlatform = "${deployPlatform}"
|
|
742
|
-
|
|
743
|
-
export default {}`;
|
|
744
|
-
writeFileSync(hybridConfig, newContent);
|
|
1977
|
+
return deployModule;
|
|
1978
|
+
}
|
|
1979
|
+
function parseDeployArgs(args) {
|
|
1980
|
+
const skipSet = /* @__PURE__ */ new Set();
|
|
1981
|
+
const valuedFlags = /* @__PURE__ */ new Set([
|
|
1982
|
+
"--provider",
|
|
1983
|
+
"-p",
|
|
1984
|
+
"--name",
|
|
1985
|
+
"-n"
|
|
1986
|
+
]);
|
|
1987
|
+
for (let i = 0; i < args.length; i++) {
|
|
1988
|
+
if (valuedFlags.has(args[i])) {
|
|
1989
|
+
skipSet.add(i);
|
|
1990
|
+
skipSet.add(i + 1);
|
|
745
1991
|
}
|
|
746
1992
|
}
|
|
747
|
-
const
|
|
748
|
-
const
|
|
749
|
-
const
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
1993
|
+
const providerIdx = args.indexOf("--provider");
|
|
1994
|
+
const providerAltIdx = args.indexOf("-p");
|
|
1995
|
+
const nameIdx = args.indexOf("--name");
|
|
1996
|
+
const nameAltIdx = args.indexOf("-n");
|
|
1997
|
+
const providerFlag = (providerIdx !== -1 ? args[providerIdx + 1] : void 0) || (providerAltIdx !== -1 ? args[providerAltIdx + 1] : void 0);
|
|
1998
|
+
const nameFlag = (nameIdx !== -1 ? args[nameIdx + 1] : void 0) || (nameAltIdx !== -1 ? args[nameAltIdx + 1] : void 0);
|
|
1999
|
+
const name = args.find(
|
|
2000
|
+
(a, i) => i > 1 && !a.startsWith("--") && !a.startsWith("-") && !skipSet.has(i) && a !== "deploy" && a !== "sleep" && a !== "wake" && a !== "status" && a !== "logs" && a !== "teardown" && a !== providerFlag && a !== nameFlag
|
|
2001
|
+
);
|
|
2002
|
+
const skipBuild = args.includes("--no-build");
|
|
2003
|
+
const force = args.includes("--force");
|
|
2004
|
+
const follow = !args.includes("--no-follow");
|
|
2005
|
+
return {
|
|
2006
|
+
platform: providerFlag,
|
|
2007
|
+
name: name || nameFlag,
|
|
2008
|
+
skipBuild,
|
|
2009
|
+
force,
|
|
2010
|
+
follow
|
|
2011
|
+
};
|
|
2012
|
+
}
|
|
2013
|
+
async function deployCommand(args) {
|
|
2014
|
+
const sub = args[1];
|
|
2015
|
+
const knownProviders = /* @__PURE__ */ new Set([
|
|
2016
|
+
"sprites",
|
|
2017
|
+
"e2b",
|
|
2018
|
+
"daytona",
|
|
2019
|
+
"northflank"
|
|
2020
|
+
]);
|
|
2021
|
+
const isPlatform = sub && !sub.startsWith("-") && knownProviders.has(sub);
|
|
2022
|
+
if (!sub || sub.startsWith("-") || isPlatform) {
|
|
2023
|
+
const flags = parseDeployArgs(args);
|
|
2024
|
+
const { runDeploy: runDeploy2 } = await loadDeploy();
|
|
2025
|
+
await runDeploy2(
|
|
2026
|
+
{
|
|
2027
|
+
platform: flags.platform || (isPlatform ? sub : void 0),
|
|
2028
|
+
name: flags.name,
|
|
2029
|
+
skipBuild: flags.skipBuild,
|
|
2030
|
+
force: flags.force
|
|
2031
|
+
},
|
|
2032
|
+
projectRoot,
|
|
2033
|
+
packageDir
|
|
2034
|
+
);
|
|
2035
|
+
return;
|
|
777
2036
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
2037
|
+
const pIdx = args.indexOf("--provider");
|
|
2038
|
+
const pAlt = args.indexOf("-p");
|
|
2039
|
+
const nIdx = args.indexOf("--name");
|
|
2040
|
+
const nAlt = args.indexOf("-n");
|
|
2041
|
+
const subPlatform = (pIdx !== -1 ? args[pIdx + 1] : void 0) || (pAlt !== -1 ? args[pAlt + 1] : void 0);
|
|
2042
|
+
const subSkipIdx = /* @__PURE__ */ new Set();
|
|
2043
|
+
subSkipIdx.add(pIdx);
|
|
2044
|
+
subSkipIdx.add(pAlt);
|
|
2045
|
+
subSkipIdx.add(nIdx);
|
|
2046
|
+
subSkipIdx.add(nAlt);
|
|
2047
|
+
if (pIdx !== -1) subSkipIdx.add(pIdx + 1);
|
|
2048
|
+
if (pAlt !== -1) subSkipIdx.add(pAlt + 1);
|
|
2049
|
+
if (nIdx !== -1) subSkipIdx.add(nIdx + 1);
|
|
2050
|
+
if (nAlt !== -1) subSkipIdx.add(nAlt + 1);
|
|
2051
|
+
const name = args.find(
|
|
2052
|
+
(a, i) => i > 1 && !a.startsWith("-") && !subSkipIdx.has(i)
|
|
2053
|
+
);
|
|
2054
|
+
switch (sub) {
|
|
2055
|
+
case "sleep": {
|
|
2056
|
+
if (!name) {
|
|
2057
|
+
console.error("Usage: hybrid deploy sleep <name>");
|
|
2058
|
+
process.exit(1);
|
|
785
2059
|
}
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
randomBytes
|
|
790
|
-
);
|
|
791
|
-
const account = privateKeyToAccount(walletKey);
|
|
792
|
-
console.log(`\u2705 Wallet: ${account.address}
|
|
793
|
-
`);
|
|
2060
|
+
const { runSleep: runSleep2 } = await loadDeploy();
|
|
2061
|
+
await runSleep2(name, subPlatform, projectRoot);
|
|
2062
|
+
break;
|
|
794
2063
|
}
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
type: "password",
|
|
800
|
-
name: "key",
|
|
801
|
-
message: "Paste OpenRouter API key:"
|
|
802
|
-
});
|
|
803
|
-
const key = result.key?.trim();
|
|
804
|
-
if (key && key.length > 0) {
|
|
805
|
-
openRouterKey = key;
|
|
806
|
-
break;
|
|
807
|
-
}
|
|
808
|
-
console.log(" OpenRouter API key is required. Please enter a value.");
|
|
2064
|
+
case "wake": {
|
|
2065
|
+
if (!name) {
|
|
2066
|
+
console.error("Usage: hybrid deploy wake <name>");
|
|
2067
|
+
process.exit(1);
|
|
809
2068
|
}
|
|
2069
|
+
const { runWake: runWake2 } = await loadDeploy();
|
|
2070
|
+
await runWake2(name, subPlatform, projectRoot);
|
|
2071
|
+
break;
|
|
810
2072
|
}
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
name: "owner",
|
|
816
|
-
message: "Your wallet address (owner):",
|
|
817
|
-
validate: (v) => /^0x[a-fA-F0-9]{40}$/.test(v) || "Enter a valid Ethereum address (0x...)"
|
|
818
|
-
});
|
|
819
|
-
if (result.owner) {
|
|
820
|
-
const { mkdirSync } = await import("fs");
|
|
821
|
-
mkdirSync(resolve(projectDir, "credentials"), { recursive: true });
|
|
822
|
-
writeFileSync(
|
|
823
|
-
aclPath,
|
|
824
|
-
JSON.stringify(
|
|
825
|
-
{ version: 1, allowFrom: [result.owner.toLowerCase()] },
|
|
826
|
-
null,
|
|
827
|
-
" "
|
|
828
|
-
)
|
|
829
|
-
);
|
|
830
|
-
console.log(`\u2705 Owner set: ${result.owner}
|
|
831
|
-
`);
|
|
2073
|
+
case "status": {
|
|
2074
|
+
if (!name) {
|
|
2075
|
+
console.error("Usage: hybrid deploy status <name>");
|
|
2076
|
+
process.exit(1);
|
|
832
2077
|
}
|
|
2078
|
+
const { runStatus: runStatus2 } = await loadDeploy();
|
|
2079
|
+
await runStatus2(name, subPlatform, projectRoot);
|
|
2080
|
+
break;
|
|
833
2081
|
}
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
if (existsSync2(projectFlyToml)) {
|
|
839
|
-
const { cpSync } = await import("fs");
|
|
840
|
-
cpSync(projectFlyToml, resolve(distDir, "fly.toml"));
|
|
841
|
-
} else {
|
|
842
|
-
const { writeFileSync: writeFileSync2 } = await import("fs");
|
|
843
|
-
const flyTomlContent = generateFlyToml(appName);
|
|
844
|
-
writeFileSync2(resolve(distDir, "fly.toml"), flyTomlContent);
|
|
845
|
-
}
|
|
846
|
-
if (!appExists) {
|
|
847
|
-
console.log(`\u{1F4E6} Creating Fly.io app: ${appName}`);
|
|
848
|
-
try {
|
|
849
|
-
execFileSync("fly", ["apps", "create", appName], {
|
|
850
|
-
cwd: distDir,
|
|
851
|
-
stdio: "inherit"
|
|
852
|
-
});
|
|
853
|
-
} catch {
|
|
854
|
-
console.log(` App may already exist, continuing...`);
|
|
2082
|
+
case "logs": {
|
|
2083
|
+
if (!name) {
|
|
2084
|
+
console.error("Usage: hybrid deploy logs <name>");
|
|
2085
|
+
process.exit(1);
|
|
855
2086
|
}
|
|
2087
|
+
const follow = !args.includes("--no-follow");
|
|
2088
|
+
const { runLogs: runLogs2 } = await loadDeploy();
|
|
2089
|
+
await runLogs2(name, follow, subPlatform, projectRoot);
|
|
2090
|
+
break;
|
|
856
2091
|
}
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
});
|
|
862
|
-
deploy2.on(
|
|
863
|
-
"error",
|
|
864
|
-
(err) => reject(new Error(`Deploy failed: ${err.message}`))
|
|
865
|
-
);
|
|
866
|
-
deploy2.on(
|
|
867
|
-
"close",
|
|
868
|
-
(code) => code === 0 ? resolve2() : reject(new Error(`Exit ${code}`))
|
|
869
|
-
);
|
|
870
|
-
});
|
|
871
|
-
if (walletKey) {
|
|
872
|
-
console.log("\n\u{1F510} Setting wallet key as secret...");
|
|
873
|
-
try {
|
|
874
|
-
execFileSync(
|
|
875
|
-
"fly",
|
|
876
|
-
["secrets", "set", `AGENT_WALLET_KEY=${walletKey}`, "--app", appName],
|
|
877
|
-
{
|
|
878
|
-
cwd: distDir,
|
|
879
|
-
stdio: "inherit"
|
|
880
|
-
}
|
|
881
|
-
);
|
|
882
|
-
} catch (e) {
|
|
883
|
-
console.log(" \u26A0\uFE0F Could not set secret, skipping...");
|
|
2092
|
+
case "teardown": {
|
|
2093
|
+
if (!name) {
|
|
2094
|
+
console.error("Usage: hybrid deploy teardown <name>");
|
|
2095
|
+
process.exit(1);
|
|
884
2096
|
}
|
|
2097
|
+
const { runTeardown: runTeardown2 } = await loadDeploy();
|
|
2098
|
+
await runTeardown2(name, subPlatform, projectRoot);
|
|
2099
|
+
break;
|
|
885
2100
|
}
|
|
886
|
-
|
|
887
|
-
console.
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
"fly",
|
|
891
|
-
["secrets", "set", `OPENROUTER_API_KEY=${openRouterKey}`, "--app", appName],
|
|
892
|
-
{
|
|
893
|
-
cwd: distDir,
|
|
894
|
-
stdio: "inherit"
|
|
895
|
-
}
|
|
896
|
-
);
|
|
897
|
-
} catch (e) {
|
|
898
|
-
console.log(" \u26A0\uFE0F Could not set secret, skipping...");
|
|
899
|
-
}
|
|
900
|
-
}
|
|
901
|
-
console.log("\n\u2705 Deployed!");
|
|
902
|
-
console.log(` Dashboard: https://fly.io/apps/${appName}`);
|
|
903
|
-
if (!existsSync2(projectFlyToml)) {
|
|
904
|
-
const { cpSync } = await import("fs");
|
|
905
|
-
cpSync(resolve(distDir, "fly.toml"), projectFlyToml);
|
|
906
|
-
console.log(` Saved fly.toml to project`);
|
|
907
|
-
}
|
|
908
|
-
return;
|
|
909
|
-
}
|
|
910
|
-
if (deployPlatform === "railway") {
|
|
911
|
-
console.log("\n\u{1F682} Railway deployment not yet implemented.");
|
|
912
|
-
console.log(" See https://railway.app for manual deployment.");
|
|
913
|
-
process.exit(1);
|
|
2101
|
+
default:
|
|
2102
|
+
console.error(`Unknown deploy subcommand: ${sub}`);
|
|
2103
|
+
printDeployHelp();
|
|
2104
|
+
process.exit(1);
|
|
914
2105
|
}
|
|
915
|
-
console.error(`Unknown platform: ${deployPlatform}`);
|
|
916
|
-
console.error("Supported: fly, railway");
|
|
917
|
-
process.exit(1);
|
|
918
2106
|
}
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
929
|
-
const { randomBytes } = await import("crypto");
|
|
930
|
-
const targetPrefix = prefix?.toLowerCase() || "";
|
|
931
|
-
if (targetPrefix && !/^[0-9a-f]+$/.test(targetPrefix)) {
|
|
932
|
-
console.error("\n\u274C Prefix must be hex characters (0-9, a-f)");
|
|
933
|
-
process.exit(1);
|
|
934
|
-
}
|
|
935
|
-
if (targetPrefix.length > 6) {
|
|
936
|
-
console.error("\n\u274C Prefix too long (max 6 characters)");
|
|
937
|
-
process.exit(1);
|
|
938
|
-
}
|
|
939
|
-
console.log("\n\u{1F511} Generating wallet...");
|
|
940
|
-
if (targetPrefix) console.log(` Looking for 0x${targetPrefix}...`);
|
|
941
|
-
const walletKey = await generateVanityWallet(
|
|
942
|
-
targetPrefix,
|
|
943
|
-
privateKeyToAccount,
|
|
944
|
-
randomBytes
|
|
2107
|
+
function printDeployHelp() {
|
|
2108
|
+
console.error("");
|
|
2109
|
+
console.error("Usage: hybrid deploy <subcommand>");
|
|
2110
|
+
console.error("");
|
|
2111
|
+
console.error("Commands:");
|
|
2112
|
+
console.error(
|
|
2113
|
+
" deploy [platform] Deploy to a Firecracker provider"
|
|
945
2114
|
);
|
|
946
|
-
|
|
947
|
-
console.
|
|
948
|
-
|
|
949
|
-
console.
|
|
950
|
-
console.
|
|
951
|
-
|
|
952
|
-
console.
|
|
953
|
-
|
|
954
|
-
`);
|
|
955
|
-
}
|
|
956
|
-
async function generateVanityWallet(prefix, privateKeyToAccount, randomBytes) {
|
|
957
|
-
let attempts = 0;
|
|
958
|
-
const max = prefix ? 1e6 : 1;
|
|
959
|
-
while (attempts < max) {
|
|
960
|
-
attempts++;
|
|
961
|
-
const key = `0x${randomBytes(32).toString("hex")}`;
|
|
962
|
-
if (!prefix) return key;
|
|
963
|
-
const { address } = privateKeyToAccount(key);
|
|
964
|
-
if (address.toLowerCase().startsWith(`0x${prefix}`)) {
|
|
965
|
-
console.log(` Found in ${attempts} attempts!`);
|
|
966
|
-
return key;
|
|
967
|
-
}
|
|
968
|
-
if (attempts % 1e3 === 0) {
|
|
969
|
-
process.stdout.write(
|
|
970
|
-
`\r Searching${".".repeat(attempts / 1e3 % 4)}${" ".repeat(3)}${attempts}\r`
|
|
971
|
-
);
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
throw new Error(`No vanity address found after ${max} attempts`);
|
|
975
|
-
}
|
|
976
|
-
async function register() {
|
|
977
|
-
const { execSync } = await import("child_process");
|
|
978
|
-
const { existsSync: existsSync2, readFileSync } = await import("fs");
|
|
979
|
-
const { join } = await import("path");
|
|
980
|
-
const { privateKeyToAccount } = await import("viem/accounts");
|
|
981
|
-
const walletKey = process.env.AGENT_WALLET_KEY || process.env.WALLET_KEY;
|
|
982
|
-
if (!walletKey) {
|
|
983
|
-
console.error("\n\u274C Set AGENT_WALLET_KEY first");
|
|
984
|
-
process.exit(1);
|
|
985
|
-
}
|
|
986
|
-
const account = privateKeyToAccount(
|
|
987
|
-
walletKey.startsWith("0x") ? walletKey : `0x${walletKey}`
|
|
2115
|
+
console.error(" deploy sleep <name> Put VM to sleep");
|
|
2116
|
+
console.error(" deploy wake <name> Wake VM");
|
|
2117
|
+
console.error(" deploy status <name> Show VM status");
|
|
2118
|
+
console.error(" deploy logs <name> Stream agent logs");
|
|
2119
|
+
console.error(" deploy teardown <name> [--all] Destroy VM");
|
|
2120
|
+
console.error("");
|
|
2121
|
+
console.error(
|
|
2122
|
+
"Flags: --provider <name> --name <name> --force --no-build --no-follow"
|
|
988
2123
|
);
|
|
989
|
-
const projectDir = projectRoot;
|
|
990
|
-
const aclPath = join(projectDir, "credentials", "xmtp-allowFrom.json");
|
|
991
|
-
if (!existsSync2(aclPath)) {
|
|
992
|
-
console.error("\n\u274C No credentials/xmtp-allowFrom.json");
|
|
993
|
-
console.error("Run 'hybrid init' first");
|
|
994
|
-
process.exit(1);
|
|
995
|
-
}
|
|
996
|
-
try {
|
|
997
|
-
const acl = JSON.parse(readFileSync(aclPath, "utf-8"));
|
|
998
|
-
if (!acl.allowFrom?.length) {
|
|
999
|
-
console.error("\n\u274C No owners in ACL");
|
|
1000
|
-
process.exit(1);
|
|
1001
|
-
}
|
|
1002
|
-
} catch {
|
|
1003
|
-
console.error("\n\u274C Invalid credentials/xmtp-allowFrom.json");
|
|
1004
|
-
process.exit(1);
|
|
1005
|
-
}
|
|
1006
|
-
console.log(`
|
|
1007
|
-
\u{1F510} Registering ${account.address} on XMTP...`);
|
|
1008
|
-
try {
|
|
1009
|
-
execSync("npx pnpm --filter @hybrd/xmtp register", {
|
|
1010
|
-
cwd: resolve(packageDir, "..", ".."),
|
|
1011
|
-
stdio: "inherit",
|
|
1012
|
-
env: { ...process.env, AGENT_WALLET_KEY: walletKey }
|
|
1013
|
-
});
|
|
1014
|
-
} catch {
|
|
1015
|
-
console.error("\n\u274C Registration failed");
|
|
1016
|
-
process.exit(1);
|
|
1017
|
-
}
|
|
1018
|
-
}
|
|
1019
|
-
async function revoke(inboxId) {
|
|
1020
|
-
const { execSync } = await import("child_process");
|
|
1021
|
-
if (!inboxId) {
|
|
1022
|
-
console.log("\nUsage: hybrid revoke <inboxId>");
|
|
1023
|
-
console.log("Or use: hybrid revoke-all\n");
|
|
1024
|
-
process.exit(1);
|
|
1025
|
-
}
|
|
1026
|
-
console.log("\n\u{1F504} Revoking XMTP installations...\n");
|
|
1027
|
-
try {
|
|
1028
|
-
execSync(`npx pnpm --filter @hybrd/xmtp revoke ${inboxId}`, {
|
|
1029
|
-
cwd: resolve(packageDir, "..", ".."),
|
|
1030
|
-
stdio: "inherit"
|
|
1031
|
-
});
|
|
1032
|
-
} catch {
|
|
1033
|
-
console.log("\n\u274C Revoke failed. Check AGENT_WALLET_KEY");
|
|
1034
|
-
process.exit(1);
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
async function revokeAll() {
|
|
1038
|
-
const { execSync } = await import("child_process");
|
|
1039
|
-
console.log("\n\u{1F504} Revoking all XMTP installations...\n");
|
|
1040
|
-
try {
|
|
1041
|
-
execSync("npx pnpm --filter @hybrd/xmtp revoke-all", {
|
|
1042
|
-
cwd: resolve(packageDir, "..", ".."),
|
|
1043
|
-
stdio: "inherit"
|
|
1044
|
-
});
|
|
1045
|
-
} catch {
|
|
1046
|
-
console.log("\n\u274C Revoke failed. Check AGENT_WALLET_KEY");
|
|
1047
|
-
process.exit(1);
|
|
1048
|
-
}
|
|
1049
2124
|
}
|
|
1050
2125
|
async function ownerAdd(address) {
|
|
1051
|
-
const { join } = await import("path");
|
|
1052
|
-
const { existsSync:
|
|
2126
|
+
const { join: join5 } = await import("path");
|
|
2127
|
+
const { existsSync: existsSync7, mkdirSync: mkdirSync2, writeFileSync: writeFileSync2, readFileSync: readFileSync2 } = await import("fs");
|
|
1053
2128
|
if (!address) {
|
|
1054
2129
|
console.error("Usage: hybrid owner add <address>");
|
|
1055
2130
|
process.exit(1);
|
|
1056
2131
|
}
|
|
1057
2132
|
const projectDir = projectRoot;
|
|
1058
|
-
const aclPath =
|
|
2133
|
+
const aclPath = join5(projectDir, "credentials", "allowFrom.json");
|
|
1059
2134
|
const normalized = address.toLowerCase().trim();
|
|
1060
|
-
|
|
2135
|
+
mkdirSync2(join5(projectDir, "credentials"), { recursive: true });
|
|
1061
2136
|
let acl = {
|
|
1062
2137
|
version: 1,
|
|
1063
2138
|
allowFrom: []
|
|
1064
2139
|
};
|
|
1065
|
-
if (
|
|
2140
|
+
if (existsSync7(aclPath)) {
|
|
1066
2141
|
try {
|
|
1067
|
-
acl = JSON.parse(
|
|
2142
|
+
acl = JSON.parse(readFileSync2(aclPath, "utf-8"));
|
|
1068
2143
|
} catch {
|
|
1069
2144
|
}
|
|
1070
2145
|
}
|
|
1071
2146
|
if (!acl.allowFrom.includes(normalized)) {
|
|
1072
2147
|
acl.allowFrom.push(normalized);
|
|
1073
|
-
|
|
2148
|
+
writeFileSync2(aclPath, JSON.stringify(acl, null, " "));
|
|
1074
2149
|
console.log(`
|
|
1075
2150
|
\u2705 Added owner: ${normalized}`);
|
|
1076
2151
|
} else {
|
|
@@ -1082,20 +2157,20 @@ async function ownerAdd(address) {
|
|
|
1082
2157
|
for (const owner of acl.allowFrom) console.log(` - ${owner}`);
|
|
1083
2158
|
}
|
|
1084
2159
|
async function ownerRemove(address) {
|
|
1085
|
-
const { join } = await import("path");
|
|
1086
|
-
const { existsSync:
|
|
2160
|
+
const { join: join5 } = await import("path");
|
|
2161
|
+
const { existsSync: existsSync7, writeFileSync: writeFileSync2, readFileSync: readFileSync2 } = await import("fs");
|
|
1087
2162
|
if (!address) {
|
|
1088
2163
|
console.error("Usage: hybrid owner remove <address>");
|
|
1089
2164
|
process.exit(1);
|
|
1090
2165
|
}
|
|
1091
2166
|
const projectDir = projectRoot;
|
|
1092
|
-
const aclPath =
|
|
1093
|
-
if (!
|
|
1094
|
-
console.error("No ACL file. Run 'hybrid
|
|
2167
|
+
const aclPath = join5(projectDir, "credentials", "allowFrom.json");
|
|
2168
|
+
if (!existsSync7(aclPath)) {
|
|
2169
|
+
console.error("No ACL file. Run 'hybrid init' first.");
|
|
1095
2170
|
process.exit(1);
|
|
1096
2171
|
}
|
|
1097
2172
|
const acl = JSON.parse(
|
|
1098
|
-
|
|
2173
|
+
readFileSync2(aclPath, "utf-8")
|
|
1099
2174
|
);
|
|
1100
2175
|
const normalized = address.toLowerCase().trim();
|
|
1101
2176
|
const index = acl.allowFrom.indexOf(normalized);
|
|
@@ -1105,7 +2180,7 @@ async function ownerRemove(address) {
|
|
|
1105
2180
|
process.exit(1);
|
|
1106
2181
|
}
|
|
1107
2182
|
acl.allowFrom.splice(index, 1);
|
|
1108
|
-
|
|
2183
|
+
writeFileSync2(aclPath, JSON.stringify(acl, null, " "));
|
|
1109
2184
|
console.log(`
|
|
1110
2185
|
\u2705 Removed owner: ${normalized}`);
|
|
1111
2186
|
if (acl.allowFrom.length > 0) {
|
|
@@ -1117,17 +2192,17 @@ async function ownerRemove(address) {
|
|
|
1117
2192
|
}
|
|
1118
2193
|
}
|
|
1119
2194
|
async function ownerList() {
|
|
1120
|
-
const { join } = await import("path");
|
|
1121
|
-
const { existsSync:
|
|
2195
|
+
const { join: join5 } = await import("path");
|
|
2196
|
+
const { existsSync: existsSync7, readFileSync: readFileSync2 } = await import("fs");
|
|
1122
2197
|
const projectDir = projectRoot;
|
|
1123
|
-
const aclPath =
|
|
1124
|
-
if (!
|
|
1125
|
-
console.log("\n\u26A0\uFE0F No ACL file. Run 'hybrid
|
|
2198
|
+
const aclPath = join5(projectDir, "credentials", "allowFrom.json");
|
|
2199
|
+
if (!existsSync7(aclPath)) {
|
|
2200
|
+
console.log("\n\u26A0\uFE0F No ACL file. Run 'hybrid init' first.");
|
|
1126
2201
|
console.log("\n Agent is open to all users.");
|
|
1127
2202
|
return;
|
|
1128
2203
|
}
|
|
1129
2204
|
const acl = JSON.parse(
|
|
1130
|
-
|
|
2205
|
+
readFileSync2(aclPath, "utf-8")
|
|
1131
2206
|
);
|
|
1132
2207
|
if (acl.allowFrom.length === 0) {
|
|
1133
2208
|
console.log("\n\u{1F4CB} No owners. Agent is open to all users.");
|