@polpo-ai/cli 0.6.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/LICENSE +13 -0
- package/dist/commands/cloud/api.js +32 -0
- package/dist/commands/cloud/api.js.map +1 -0
- package/dist/commands/cloud/byok.js +125 -0
- package/dist/commands/cloud/byok.js.map +1 -0
- package/dist/commands/cloud/config.js +42 -0
- package/dist/commands/cloud/config.js.map +1 -0
- package/dist/commands/cloud/deploy.js +752 -0
- package/dist/commands/cloud/deploy.js.map +1 -0
- package/dist/commands/cloud/login.js +99 -0
- package/dist/commands/cloud/login.js.map +1 -0
- package/dist/commands/cloud/logout.js +11 -0
- package/dist/commands/cloud/logout.js.map +1 -0
- package/dist/commands/cloud/logs.js +114 -0
- package/dist/commands/cloud/logs.js.map +1 -0
- package/dist/commands/cloud/project-context.js +17 -0
- package/dist/commands/cloud/project-context.js.map +1 -0
- package/dist/commands/cloud/projects.js +95 -0
- package/dist/commands/cloud/projects.js.map +1 -0
- package/dist/commands/cloud/prompt.js +74 -0
- package/dist/commands/cloud/prompt.js.map +1 -0
- package/dist/commands/cloud/status.js +84 -0
- package/dist/commands/cloud/status.js.map +1 -0
- package/dist/commands/create.js +286 -0
- package/dist/commands/create.js.map +1 -0
- package/dist/commands/link.js +71 -0
- package/dist/commands/link.js.map +1 -0
- package/dist/commands/models.js +60 -0
- package/dist/commands/models.js.map +1 -0
- package/dist/commands/orgs.js +42 -0
- package/dist/commands/orgs.js.map +1 -0
- package/dist/commands/update.js +95 -0
- package/dist/commands/update.js.map +1 -0
- package/dist/commands/whoami.js +62 -0
- package/dist/commands/whoami.js.map +1 -0
- package/dist/index.js +138 -0
- package/dist/index.js.map +1 -0
- package/dist/interactive-menu.js +111 -0
- package/dist/interactive-menu.js.map +1 -0
- package/dist/update-check.js +106 -0
- package/dist/update-check.js.map +1 -0
- package/dist/util/api-keys.js +21 -0
- package/dist/util/api-keys.js.map +1 -0
- package/dist/util/auth.js +71 -0
- package/dist/util/auth.js.map +1 -0
- package/dist/util/base-url.js +43 -0
- package/dist/util/base-url.js.map +1 -0
- package/dist/util/browser.js +20 -0
- package/dist/util/browser.js.map +1 -0
- package/dist/util/device-code.js +103 -0
- package/dist/util/device-code.js.map +1 -0
- package/dist/util/errors.js +13 -0
- package/dist/util/errors.js.map +1 -0
- package/dist/util/install-cli.js +68 -0
- package/dist/util/install-cli.js.map +1 -0
- package/dist/util/org.js +53 -0
- package/dist/util/org.js.map +1 -0
- package/dist/util/polpo-config.js +39 -0
- package/dist/util/polpo-config.js.map +1 -0
- package/dist/util/project.js +87 -0
- package/dist/util/project.js.map +1 -0
- package/dist/util/skills.js +53 -0
- package/dist/util/skills.js.map +1 -0
- package/dist/util/slugify.js +19 -0
- package/dist/util/slugify.js.map +1 -0
- package/dist/util/template.js +118 -0
- package/dist/util/template.js.map +1 -0
- package/package.json +38 -0
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* polpo deploy — sync local .polpo/ project to cloud.
|
|
3
|
+
*
|
|
4
|
+
* Core (always deployed):
|
|
5
|
+
* - agents.json (agent definitions)
|
|
6
|
+
* - teams.json (team structure)
|
|
7
|
+
* - memory.md + memory/<agent>.md (knowledge base)
|
|
8
|
+
* - playbooks/ (mission templates)
|
|
9
|
+
* - missions/ (mission definitions)
|
|
10
|
+
* - skills/ (SKILL.md files)
|
|
11
|
+
* - vault.enc (encrypted credentials)
|
|
12
|
+
*
|
|
13
|
+
* Opt-in (with flags):
|
|
14
|
+
* --include-tasks Deploy tasks
|
|
15
|
+
* --include-sessions Deploy chat sessions
|
|
16
|
+
* --all Deploy everything (seamless local→cloud migration)
|
|
17
|
+
*/
|
|
18
|
+
import * as fs from "node:fs";
|
|
19
|
+
import * as path from "node:path";
|
|
20
|
+
import { loadCredentials } from "./config.js";
|
|
21
|
+
import { createApiClient } from "./api.js";
|
|
22
|
+
import { isTTY, confirm } from "./prompt.js";
|
|
23
|
+
import { resolveKey, decrypt } from "@polpo-ai/vault-crypto";
|
|
24
|
+
import { AddAgentSchema } from "@polpo-ai/server";
|
|
25
|
+
import { friendlyError } from "../../util/errors.js";
|
|
26
|
+
import { resolveDefaultOrg } from "../../util/org.js";
|
|
27
|
+
import { resolveOrCreateProject } from "../../util/project.js";
|
|
28
|
+
function emptyResult() {
|
|
29
|
+
return { created: 0, updated: 0, skipped: 0, failed: 0, errors: [] };
|
|
30
|
+
}
|
|
31
|
+
function mergeResult(target, source) {
|
|
32
|
+
target.created += source.created;
|
|
33
|
+
target.updated += source.updated;
|
|
34
|
+
target.skipped += source.skipped;
|
|
35
|
+
target.failed += source.failed;
|
|
36
|
+
target.errors.push(...source.errors);
|
|
37
|
+
}
|
|
38
|
+
// ── Helpers ──────────────────────────────────────────────
|
|
39
|
+
function resolvePolpoDir(dir) {
|
|
40
|
+
const polpoDir = path.resolve(dir, ".polpo");
|
|
41
|
+
if (!fs.existsSync(polpoDir)) {
|
|
42
|
+
console.error(`Error: .polpo/ directory not found in ${path.resolve(dir)}`);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
return polpoDir;
|
|
46
|
+
}
|
|
47
|
+
function loadJson(filePath) {
|
|
48
|
+
if (!fs.existsSync(filePath))
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8"));
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
console.error(`Warning: Could not parse ${filePath}`);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function loadText(filePath) {
|
|
59
|
+
if (!fs.existsSync(filePath))
|
|
60
|
+
return null;
|
|
61
|
+
return fs.readFileSync(filePath, "utf-8");
|
|
62
|
+
}
|
|
63
|
+
function listJsonFiles(dir) {
|
|
64
|
+
if (!fs.existsSync(dir))
|
|
65
|
+
return [];
|
|
66
|
+
return fs.readdirSync(dir).filter(f => f.endsWith(".json")).map(f => path.join(dir, f));
|
|
67
|
+
}
|
|
68
|
+
// ── Core deployers ──────────────────────────────────────
|
|
69
|
+
async function deployTeams(client, polpoDir) {
|
|
70
|
+
const result = emptyResult();
|
|
71
|
+
const teams = loadJson(path.join(polpoDir, "teams.json"));
|
|
72
|
+
if (!teams || !Array.isArray(teams))
|
|
73
|
+
return result;
|
|
74
|
+
for (const team of teams) {
|
|
75
|
+
if (!team.name || typeof team.name !== "string") {
|
|
76
|
+
result.errors.push(`team missing "name" field`);
|
|
77
|
+
result.failed++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const res = await client.post("/v1/agents/teams", { name: team.name, description: team.description });
|
|
81
|
+
if (res.status >= 200 && res.status < 300) {
|
|
82
|
+
result.created++;
|
|
83
|
+
}
|
|
84
|
+
else if (res.status === 409 || res.data?.error?.includes("already exists")) {
|
|
85
|
+
result.skipped++;
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
const msg = res.data?.error ?? `HTTP ${res.status}`;
|
|
89
|
+
result.errors.push(`team "${team.name}": ${friendlyError(msg)}`);
|
|
90
|
+
result.failed++;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return result;
|
|
94
|
+
}
|
|
95
|
+
async function deployAgents(client, polpoDir, force) {
|
|
96
|
+
const result = emptyResult();
|
|
97
|
+
const raw = loadJson(path.join(polpoDir, "agents.json"));
|
|
98
|
+
if (!raw || !Array.isArray(raw)) {
|
|
99
|
+
if (raw && !Array.isArray(raw)) {
|
|
100
|
+
result.errors.push("agents.json must be a JSON array, e.g. [{ \"agent\": { \"name\": \"...\", ... }, \"teamName\": \"default\" }]");
|
|
101
|
+
result.failed++;
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
// Fetch existing agents for upsert detection
|
|
106
|
+
let existingNames = new Set();
|
|
107
|
+
try {
|
|
108
|
+
const res = await client.get("/v1/agents");
|
|
109
|
+
if (res.status === 200) {
|
|
110
|
+
const data = res.data?.data ?? res.data ?? [];
|
|
111
|
+
if (Array.isArray(data))
|
|
112
|
+
existingNames = new Set(data.map((a) => a.name));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
catch { /* can't check — will try create */ }
|
|
116
|
+
for (const entry of raw) {
|
|
117
|
+
const agent = entry.agent ?? entry;
|
|
118
|
+
const teamName = entry.teamName ?? "default";
|
|
119
|
+
// Validate agent schema
|
|
120
|
+
const parsed = AddAgentSchema.safeParse(agent);
|
|
121
|
+
if (!parsed.success) {
|
|
122
|
+
const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
|
|
123
|
+
result.errors.push(`agent "${agent.name ?? "unknown"}": ${issues}`);
|
|
124
|
+
result.failed++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
const exists = existingNames.has(agent.name);
|
|
128
|
+
if (exists) {
|
|
129
|
+
if (!force && isTTY()) {
|
|
130
|
+
const ok = await confirm(` Agent "${agent.name}" already exists. Override?`);
|
|
131
|
+
if (!ok) {
|
|
132
|
+
result.skipped++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const res = await client.put(`/v1/agents/${encodeURIComponent(agent.name)}`, { ...agent, team: teamName });
|
|
137
|
+
if (res.status >= 200 && res.status < 300) {
|
|
138
|
+
result.updated++;
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
const msg = res.data?.error ?? `HTTP ${res.status}`;
|
|
142
|
+
result.errors.push(`agent "${agent.name}": update failed — ${friendlyError(msg)}`);
|
|
143
|
+
result.failed++;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
const res = await client.post("/v1/agents", { ...agent, team: teamName });
|
|
148
|
+
if (res.status >= 200 && res.status < 300) {
|
|
149
|
+
result.created++;
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
const msg = res.data?.error ?? `HTTP ${res.status}`;
|
|
153
|
+
result.errors.push(`agent "${agent.name}": create failed — ${friendlyError(msg)}`);
|
|
154
|
+
result.failed++;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return result;
|
|
159
|
+
}
|
|
160
|
+
async function deployMemory(client, polpoDir) {
|
|
161
|
+
const result = emptyResult();
|
|
162
|
+
const shared = loadText(path.join(polpoDir, "memory.md"));
|
|
163
|
+
if (shared) {
|
|
164
|
+
const res = await client.put("/v1/memory", { content: shared });
|
|
165
|
+
if (res.status >= 200 && res.status < 300) {
|
|
166
|
+
result.updated++;
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
result.errors.push(`memory: ${friendlyError(res.data?.error ?? `HTTP ${res.status}`)}`);
|
|
170
|
+
result.failed++;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
const memDir = path.join(polpoDir, "memory");
|
|
174
|
+
if (fs.existsSync(memDir)) {
|
|
175
|
+
for (const file of fs.readdirSync(memDir).filter(f => f.endsWith(".md"))) {
|
|
176
|
+
const agentName = file.replace(".md", "");
|
|
177
|
+
const content = loadText(path.join(memDir, file));
|
|
178
|
+
if (content) {
|
|
179
|
+
const res = await client.put(`/v1/memory/agent/${agentName}`, { content });
|
|
180
|
+
if (res.status >= 200 && res.status < 300) {
|
|
181
|
+
result.updated++;
|
|
182
|
+
}
|
|
183
|
+
else {
|
|
184
|
+
result.errors.push(`memory "${agentName}": ${friendlyError(res.data?.error ?? `HTTP ${res.status}`)}`);
|
|
185
|
+
result.failed++;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
async function deployMissions(client, polpoDir) {
|
|
193
|
+
const result = emptyResult();
|
|
194
|
+
const files = listJsonFiles(path.join(polpoDir, "missions"));
|
|
195
|
+
for (const file of files) {
|
|
196
|
+
const mission = loadJson(file);
|
|
197
|
+
if (!mission)
|
|
198
|
+
continue;
|
|
199
|
+
const res = await client.post("/v1/missions", {
|
|
200
|
+
name: mission.name,
|
|
201
|
+
data: typeof mission.data === "string" ? mission.data : JSON.stringify(mission.data),
|
|
202
|
+
prompt: mission.prompt,
|
|
203
|
+
status: mission.status ?? "draft",
|
|
204
|
+
schedule: mission.schedule,
|
|
205
|
+
deadline: mission.deadline,
|
|
206
|
+
notifications: mission.notifications,
|
|
207
|
+
});
|
|
208
|
+
if (res.status >= 200 && res.status < 300) {
|
|
209
|
+
result.created++;
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
const msg = res.data?.error ?? `HTTP ${res.status}`;
|
|
213
|
+
result.errors.push(`mission "${mission.name ?? path.basename(file)}": ${friendlyError(msg)}`);
|
|
214
|
+
result.failed++;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return result;
|
|
218
|
+
}
|
|
219
|
+
async function deployPlaybooks(client, polpoDir) {
|
|
220
|
+
const result = emptyResult();
|
|
221
|
+
const playbooksDir = path.join(polpoDir, "playbooks");
|
|
222
|
+
if (!fs.existsSync(playbooksDir))
|
|
223
|
+
return result;
|
|
224
|
+
for (const entry of fs.readdirSync(playbooksDir, { withFileTypes: true })) {
|
|
225
|
+
if (!entry.isDirectory())
|
|
226
|
+
continue;
|
|
227
|
+
const pbFile = path.join(playbooksDir, entry.name, "playbook.json");
|
|
228
|
+
const playbook = loadJson(pbFile);
|
|
229
|
+
if (!playbook)
|
|
230
|
+
continue;
|
|
231
|
+
const res = await client.post("/v1/playbooks", {
|
|
232
|
+
name: playbook.name ?? entry.name,
|
|
233
|
+
description: playbook.description,
|
|
234
|
+
mission: typeof playbook.mission === "string" ? playbook.mission : JSON.stringify(playbook.mission),
|
|
235
|
+
parameters: playbook.parameters,
|
|
236
|
+
});
|
|
237
|
+
if (res.status >= 200 && res.status < 300) {
|
|
238
|
+
result.created++;
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
const msg = res.data?.error ?? `HTTP ${res.status}`;
|
|
242
|
+
result.errors.push(`playbook "${entry.name}": ${friendlyError(msg)}`);
|
|
243
|
+
result.failed++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
248
|
+
async function deploySkills(client, polpoDir, force) {
|
|
249
|
+
const result = emptyResult();
|
|
250
|
+
const skillsDir = path.join(polpoDir, "skills");
|
|
251
|
+
if (!fs.existsSync(skillsDir))
|
|
252
|
+
return result;
|
|
253
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
254
|
+
if (!entry.isDirectory())
|
|
255
|
+
continue;
|
|
256
|
+
const skillFile = path.join(skillsDir, entry.name, "SKILL.md");
|
|
257
|
+
if (!fs.existsSync(skillFile))
|
|
258
|
+
continue;
|
|
259
|
+
const raw = fs.readFileSync(skillFile, "utf-8");
|
|
260
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---/);
|
|
261
|
+
let name = entry.name;
|
|
262
|
+
let description = "";
|
|
263
|
+
let allowedTools;
|
|
264
|
+
if (fmMatch) {
|
|
265
|
+
const lines = fmMatch[1].split("\n");
|
|
266
|
+
let currentArray = null;
|
|
267
|
+
for (const line of lines) {
|
|
268
|
+
const arrayItem = line.match(/^\s+-\s+(.+)/);
|
|
269
|
+
if (arrayItem && currentArray) {
|
|
270
|
+
currentArray.push(arrayItem[1].trim());
|
|
271
|
+
continue;
|
|
272
|
+
}
|
|
273
|
+
currentArray = null;
|
|
274
|
+
const kv = line.match(/^(\w[\w-]*)\s*:\s*(.+)?/);
|
|
275
|
+
if (kv) {
|
|
276
|
+
const key = kv[1];
|
|
277
|
+
const val = kv[2]?.trim();
|
|
278
|
+
if (key === "name" && val)
|
|
279
|
+
name = val;
|
|
280
|
+
if (key === "description" && val)
|
|
281
|
+
description = val;
|
|
282
|
+
if (key === "allowed-tools" || key === "allowedTools") {
|
|
283
|
+
allowedTools = [];
|
|
284
|
+
currentArray = allowedTools;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const bodyMatch = raw.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
|
|
290
|
+
const content = bodyMatch ? bodyMatch[1].trim() : raw.trim();
|
|
291
|
+
// Try create first
|
|
292
|
+
const res = await client.post("/v1/skills/create", {
|
|
293
|
+
name, description, content,
|
|
294
|
+
...(allowedTools?.length ? { allowedTools } : {}),
|
|
295
|
+
});
|
|
296
|
+
if (res.status >= 200 && res.status < 300) {
|
|
297
|
+
result.created++;
|
|
298
|
+
}
|
|
299
|
+
else if (res.status === 409 || res.data?.error?.includes("already exists")) {
|
|
300
|
+
if (force) {
|
|
301
|
+
// Update existing skill
|
|
302
|
+
const updateRes = await client.put(`/v1/skills/${encodeURIComponent(name)}`, {
|
|
303
|
+
description, content,
|
|
304
|
+
...(allowedTools?.length ? { allowedTools } : {}),
|
|
305
|
+
});
|
|
306
|
+
if (updateRes.status >= 200 && updateRes.status < 300) {
|
|
307
|
+
result.updated++;
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
result.skipped++;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
result.skipped++;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
else {
|
|
318
|
+
const msg = res.data?.error ?? `HTTP ${res.status}`;
|
|
319
|
+
result.errors.push(`skill "${name}": ${friendlyError(msg)}`);
|
|
320
|
+
result.failed++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return result;
|
|
324
|
+
}
|
|
325
|
+
async function deployVault(client, polpoDir) {
|
|
326
|
+
const result = emptyResult();
|
|
327
|
+
const vaultPath = path.join(polpoDir, "vault.enc");
|
|
328
|
+
if (!fs.existsSync(vaultPath))
|
|
329
|
+
return result;
|
|
330
|
+
let key;
|
|
331
|
+
try {
|
|
332
|
+
key = resolveKey();
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
result.errors.push(`vault: cannot resolve key — ${err.message}. Set POLPO_VAULT_KEY or ensure ~/.polpo/vault.key exists.`);
|
|
336
|
+
result.failed++;
|
|
337
|
+
return result;
|
|
338
|
+
}
|
|
339
|
+
let vaultData;
|
|
340
|
+
try {
|
|
341
|
+
const plaintext = decrypt(fs.readFileSync(vaultPath), key);
|
|
342
|
+
vaultData = JSON.parse(plaintext.toString("utf-8"));
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
result.errors.push(`vault: cannot decrypt — ${err.message}`);
|
|
346
|
+
result.failed++;
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
for (const [agent, services] of Object.entries(vaultData)) {
|
|
350
|
+
for (const [service, entry] of Object.entries(services)) {
|
|
351
|
+
const res = await client.post("/v1/vault/entries", {
|
|
352
|
+
agent, service,
|
|
353
|
+
type: entry.type ?? "custom",
|
|
354
|
+
label: entry.label,
|
|
355
|
+
credentials: entry.credentials,
|
|
356
|
+
});
|
|
357
|
+
if (res.status >= 200 && res.status < 300) {
|
|
358
|
+
result.created++;
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
const msg = res.data?.error ?? `HTTP ${res.status}`;
|
|
362
|
+
result.errors.push(`vault "${agent}/${service}": ${friendlyError(msg)}`);
|
|
363
|
+
result.failed++;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
async function deployAvatars(client, polpoDir, baseUrl, apiKey) {
|
|
370
|
+
const result = emptyResult();
|
|
371
|
+
const avatarsDir = path.join(polpoDir, "avatars");
|
|
372
|
+
if (!fs.existsSync(avatarsDir))
|
|
373
|
+
return result;
|
|
374
|
+
const files = fs.readdirSync(avatarsDir).filter(f => {
|
|
375
|
+
const ext = path.extname(f).toLowerCase();
|
|
376
|
+
return [".jpg", ".jpeg", ".png", ".webp", ".gif", ".svg"].includes(ext);
|
|
377
|
+
});
|
|
378
|
+
if (files.length === 0)
|
|
379
|
+
return result;
|
|
380
|
+
try {
|
|
381
|
+
await fetch(`${baseUrl}/v1/files/mkdir`, {
|
|
382
|
+
method: "POST",
|
|
383
|
+
headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
|
384
|
+
body: JSON.stringify({ path: ".polpo/avatars" }),
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch { /* may already exist */ }
|
|
388
|
+
for (const file of files) {
|
|
389
|
+
const formData = new FormData();
|
|
390
|
+
formData.append("path", ".polpo/avatars");
|
|
391
|
+
formData.append("file", new Blob([fs.readFileSync(path.join(avatarsDir, file))]), file);
|
|
392
|
+
try {
|
|
393
|
+
const res = await fetch(`${baseUrl}/v1/files/upload`, {
|
|
394
|
+
method: "POST",
|
|
395
|
+
headers: { "Authorization": `Bearer ${apiKey}` },
|
|
396
|
+
body: formData,
|
|
397
|
+
});
|
|
398
|
+
if (res.ok) {
|
|
399
|
+
result.created++;
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
result.errors.push(`avatar "${file}": HTTP ${res.status}`);
|
|
403
|
+
result.failed++;
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch (err) {
|
|
407
|
+
result.errors.push(`avatar "${file}": ${err.message}`);
|
|
408
|
+
result.failed++;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return result;
|
|
412
|
+
}
|
|
413
|
+
// ── Opt-in deployers ──────────────────────────────────────
|
|
414
|
+
async function deploySchedules(client, polpoDir) {
|
|
415
|
+
const result = emptyResult();
|
|
416
|
+
const files = listJsonFiles(path.join(polpoDir, "schedules"));
|
|
417
|
+
for (const file of files) {
|
|
418
|
+
const schedule = loadJson(file);
|
|
419
|
+
if (!schedule)
|
|
420
|
+
continue;
|
|
421
|
+
const res = await client.post("/v1/schedules", schedule);
|
|
422
|
+
if (res.status >= 200 && res.status < 300) {
|
|
423
|
+
result.created++;
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
const msg = res.data?.error ?? `HTTP ${res.status}`;
|
|
427
|
+
result.errors.push(`schedule "${schedule.name ?? path.basename(file)}": ${friendlyError(msg)}`);
|
|
428
|
+
result.failed++;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
return result;
|
|
432
|
+
}
|
|
433
|
+
async function deployTasks(client, polpoDir) {
|
|
434
|
+
const result = emptyResult();
|
|
435
|
+
const files = listJsonFiles(path.join(polpoDir, "tasks"));
|
|
436
|
+
for (const file of files) {
|
|
437
|
+
const task = loadJson(file);
|
|
438
|
+
if (!task)
|
|
439
|
+
continue;
|
|
440
|
+
const res = await client.post("/v1/tasks", {
|
|
441
|
+
title: task.title, description: task.description,
|
|
442
|
+
assignTo: task.assignTo, group: task.group,
|
|
443
|
+
missionId: task.missionId, dependsOn: task.dependsOn,
|
|
444
|
+
expectations: task.expectations, metrics: task.metrics,
|
|
445
|
+
maxRetries: task.maxRetries, maxDuration: task.maxDuration,
|
|
446
|
+
deadline: task.deadline, priority: task.priority,
|
|
447
|
+
});
|
|
448
|
+
if (res.status >= 200 && res.status < 300) {
|
|
449
|
+
result.created++;
|
|
450
|
+
}
|
|
451
|
+
else {
|
|
452
|
+
const msg = res.data?.error ?? `HTTP ${res.status}`;
|
|
453
|
+
result.errors.push(`task "${task.title ?? path.basename(file)}": ${friendlyError(msg)}`);
|
|
454
|
+
result.failed++;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return result;
|
|
458
|
+
}
|
|
459
|
+
async function deploySessions(client, polpoDir) {
|
|
460
|
+
const result = emptyResult();
|
|
461
|
+
const sessionsDir = path.join(polpoDir, "sessions");
|
|
462
|
+
if (!fs.existsSync(sessionsDir))
|
|
463
|
+
return result;
|
|
464
|
+
const files = fs.readdirSync(sessionsDir).filter(f => f.endsWith(".jsonl"));
|
|
465
|
+
for (const file of files) {
|
|
466
|
+
const raw = fs.readFileSync(path.join(sessionsDir, file), "utf-8");
|
|
467
|
+
const lines = raw.split("\n").filter(l => l.trim());
|
|
468
|
+
if (lines.length === 0)
|
|
469
|
+
continue;
|
|
470
|
+
let title;
|
|
471
|
+
let agent;
|
|
472
|
+
const messages = [];
|
|
473
|
+
for (const line of lines) {
|
|
474
|
+
try {
|
|
475
|
+
const obj = JSON.parse(line);
|
|
476
|
+
if (obj._session) {
|
|
477
|
+
title = obj.title;
|
|
478
|
+
agent = obj.agent;
|
|
479
|
+
}
|
|
480
|
+
else if (obj.role && obj.content) {
|
|
481
|
+
messages.push({ role: obj.role, content: obj.content, ...(obj.toolCalls ? { toolCalls: obj.toolCalls } : {}) });
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
catch { /* skip malformed lines */ }
|
|
485
|
+
}
|
|
486
|
+
if (messages.length === 0)
|
|
487
|
+
continue;
|
|
488
|
+
const res = await client.post("/v1/chat/sessions/import", { title, agent, messages });
|
|
489
|
+
if (res.status >= 200 && res.status < 300) {
|
|
490
|
+
result.created++;
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
const msg = res.data?.error ?? `HTTP ${res.status}`;
|
|
494
|
+
result.errors.push(`session "${title ?? file}": ${friendlyError(msg)}`);
|
|
495
|
+
result.failed++;
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
return result;
|
|
499
|
+
}
|
|
500
|
+
// ── Main command ──────────────────────────────────────
|
|
501
|
+
export function registerDeployCommand(program) {
|
|
502
|
+
program
|
|
503
|
+
.command("deploy")
|
|
504
|
+
.description("Deploy local .polpo/ project to cloud")
|
|
505
|
+
.option("-d, --dir <path>", "Project directory", ".")
|
|
506
|
+
.option("-y, --yes", "Skip all confirmation prompts")
|
|
507
|
+
.option("-f, --force", "Force override existing resources without asking")
|
|
508
|
+
.option("--include-tasks", "Also deploy tasks")
|
|
509
|
+
.option("--include-sessions", "Also deploy chat sessions")
|
|
510
|
+
.option("--all", "Deploy everything (full local→cloud migration)")
|
|
511
|
+
.action(async (opts) => {
|
|
512
|
+
const creds = loadCredentials();
|
|
513
|
+
if (!creds) {
|
|
514
|
+
console.error("Not logged in. Run: polpo login");
|
|
515
|
+
process.exit(1);
|
|
516
|
+
}
|
|
517
|
+
const polpoDir = resolvePolpoDir(opts.dir);
|
|
518
|
+
const polpoConfig = loadJson(path.join(polpoDir, "polpo.json"));
|
|
519
|
+
const projectName = polpoConfig?.project ?? path.basename(path.resolve(opts.dir));
|
|
520
|
+
const force = opts.force || opts.yes || false;
|
|
521
|
+
// Control plane client (no project context needed for orgs/projects)
|
|
522
|
+
const cpClient = createApiClient(creds);
|
|
523
|
+
console.log("\n Polpo Deploy\n");
|
|
524
|
+
// ── Step 1: Resolve project ────────────────────────
|
|
525
|
+
let projectId = polpoConfig?.projectId;
|
|
526
|
+
let projectSlug = polpoConfig?.projectSlug;
|
|
527
|
+
if (!projectId) {
|
|
528
|
+
try {
|
|
529
|
+
const org = await resolveDefaultOrg(cpClient);
|
|
530
|
+
const project = await resolveOrCreateProject({
|
|
531
|
+
client: cpClient,
|
|
532
|
+
orgId: org.id,
|
|
533
|
+
name: projectName,
|
|
534
|
+
force,
|
|
535
|
+
interactive: isTTY(),
|
|
536
|
+
});
|
|
537
|
+
projectId = project.id;
|
|
538
|
+
projectSlug = project.slug;
|
|
539
|
+
console.log(` Project: ${project.name}\n`);
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
543
|
+
console.error(` ${friendlyError(msg)}`);
|
|
544
|
+
process.exit(1);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
if (!projectId) {
|
|
548
|
+
console.error(" No project resolved. Deploy from a project directory with .polpo/polpo.json");
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
// Backfill `projectSlug` for users with legacy polpo.json (id only).
|
|
552
|
+
if (!projectSlug && projectId) {
|
|
553
|
+
try {
|
|
554
|
+
const fresh = await import("../../util/project.js").then((m) => m.getProject(cpClient, projectId));
|
|
555
|
+
if (fresh?.slug)
|
|
556
|
+
projectSlug = fresh.slug;
|
|
557
|
+
}
|
|
558
|
+
catch { }
|
|
559
|
+
}
|
|
560
|
+
const client = createApiClient(creds, projectId);
|
|
561
|
+
// Persist whichever fields we resolved/discovered for next time.
|
|
562
|
+
if (polpoConfig && (!polpoConfig.projectId || (projectSlug && !polpoConfig.projectSlug))) {
|
|
563
|
+
polpoConfig.projectId = projectId;
|
|
564
|
+
if (projectSlug)
|
|
565
|
+
polpoConfig.projectSlug = projectSlug;
|
|
566
|
+
fs.writeFileSync(path.join(polpoDir, "polpo.json"), JSON.stringify(polpoConfig, null, 2), "utf-8");
|
|
567
|
+
}
|
|
568
|
+
// ── Step 2: Detect LLM keys ────────────────────────
|
|
569
|
+
const LLM_KEYS = {
|
|
570
|
+
ANTHROPIC_API_KEY: "anthropic",
|
|
571
|
+
OPENAI_API_KEY: "openai",
|
|
572
|
+
GEMINI_API_KEY: "google",
|
|
573
|
+
XAI_API_KEY: "xai",
|
|
574
|
+
GROQ_API_KEY: "groq",
|
|
575
|
+
OPENROUTER_API_KEY: "openrouter",
|
|
576
|
+
MISTRAL_API_KEY: "mistral",
|
|
577
|
+
CEREBRAS_API_KEY: "cerebras",
|
|
578
|
+
MINIMAX_API_KEY: "minimax",
|
|
579
|
+
HF_TOKEN: "huggingface",
|
|
580
|
+
AZURE_OPENAI_API_KEY: "azure-openai-responses",
|
|
581
|
+
};
|
|
582
|
+
const detected = [];
|
|
583
|
+
for (const [envVar, provider] of Object.entries(LLM_KEYS)) {
|
|
584
|
+
if (process.env[envVar]) {
|
|
585
|
+
detected.push({ envVar, provider, value: process.env[envVar] });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
const envFile = path.join(polpoDir, ".env");
|
|
589
|
+
if (fs.existsSync(envFile)) {
|
|
590
|
+
for (const line of fs.readFileSync(envFile, "utf-8").split("\n")) {
|
|
591
|
+
const t = line.trim();
|
|
592
|
+
if (!t || t.startsWith("#"))
|
|
593
|
+
continue;
|
|
594
|
+
const eq = t.indexOf("=");
|
|
595
|
+
if (eq === -1)
|
|
596
|
+
continue;
|
|
597
|
+
const k = t.slice(0, eq).trim();
|
|
598
|
+
const v = t.slice(eq + 1).trim().replace(/^["']|["']$/g, "");
|
|
599
|
+
if (LLM_KEYS[k] && v && !detected.find(d => d.envVar === k)) {
|
|
600
|
+
detected.push({ envVar: k, provider: LLM_KEYS[k], value: v });
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (detected.length > 0) {
|
|
605
|
+
console.log(" Detected LLM keys:");
|
|
606
|
+
for (const { envVar, value } of detected) {
|
|
607
|
+
console.log(` ${envVar.padEnd(25)} ${value.slice(0, 8)}...${value.slice(-4)}`);
|
|
608
|
+
}
|
|
609
|
+
console.log();
|
|
610
|
+
if (isTTY() && !force) {
|
|
611
|
+
const push = await confirm(" Push LLM keys to cloud?");
|
|
612
|
+
if (push) {
|
|
613
|
+
let n = 0;
|
|
614
|
+
for (const { provider, value } of detected) {
|
|
615
|
+
try {
|
|
616
|
+
await cpClient.post("/v1/byok", { provider, key: value });
|
|
617
|
+
n++;
|
|
618
|
+
}
|
|
619
|
+
catch { }
|
|
620
|
+
}
|
|
621
|
+
if (n > 0)
|
|
622
|
+
console.log(` Pushed ${n} LLM key(s)\n`);
|
|
623
|
+
}
|
|
624
|
+
else {
|
|
625
|
+
console.log();
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
// ── Step 3: Scan & show resources ────────────────────
|
|
630
|
+
const hasTeams = fs.existsSync(path.join(polpoDir, "teams.json"));
|
|
631
|
+
const hasAgents = fs.existsSync(path.join(polpoDir, "agents.json"));
|
|
632
|
+
const hasMemory = fs.existsSync(path.join(polpoDir, "memory.md")) ||
|
|
633
|
+
fs.existsSync(path.join(polpoDir, "memory"));
|
|
634
|
+
const hasMissions = fs.existsSync(path.join(polpoDir, "missions")) &&
|
|
635
|
+
fs.readdirSync(path.join(polpoDir, "missions")).length > 0;
|
|
636
|
+
const hasPlaybooks = fs.existsSync(path.join(polpoDir, "playbooks"));
|
|
637
|
+
const hasSkills = fs.existsSync(path.join(polpoDir, "skills")) &&
|
|
638
|
+
fs.readdirSync(path.join(polpoDir, "skills")).some((d) => fs.statSync(path.join(polpoDir, "skills", d)).isDirectory());
|
|
639
|
+
const hasSchedules = fs.existsSync(path.join(polpoDir, "schedules")) &&
|
|
640
|
+
fs.readdirSync(path.join(polpoDir, "schedules")).length > 0;
|
|
641
|
+
const hasVault = fs.existsSync(path.join(polpoDir, "vault.enc"));
|
|
642
|
+
const hasAvatars = fs.existsSync(path.join(polpoDir, "avatars")) &&
|
|
643
|
+
fs.readdirSync(path.join(polpoDir, "avatars")).length > 0;
|
|
644
|
+
const hasTasks = fs.existsSync(path.join(polpoDir, "tasks")) &&
|
|
645
|
+
fs.readdirSync(path.join(polpoDir, "tasks")).length > 0;
|
|
646
|
+
const hasSessions = fs.existsSync(path.join(polpoDir, "sessions")) &&
|
|
647
|
+
fs.readdirSync(path.join(polpoDir, "sessions")).length > 0;
|
|
648
|
+
const includeTasks = opts.all || opts.includeTasks;
|
|
649
|
+
const includeSessions = opts.all || opts.includeSessions;
|
|
650
|
+
console.log(" Resources to deploy:");
|
|
651
|
+
if (hasAgents) {
|
|
652
|
+
const agentsData = loadJson(path.join(polpoDir, "agents.json"));
|
|
653
|
+
if (Array.isArray(agentsData)) {
|
|
654
|
+
const names = agentsData.map((e) => (e.agent ?? e).name).filter(Boolean);
|
|
655
|
+
console.log(` Agents .......... ${names.length} (${names.join(", ")})`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
if (hasTeams) {
|
|
659
|
+
const teamsData = loadJson(path.join(polpoDir, "teams.json"));
|
|
660
|
+
if (Array.isArray(teamsData)) {
|
|
661
|
+
console.log(` Teams ........... ${teamsData.length} (${teamsData.map((t) => t.name).join(", ")})`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (hasMemory)
|
|
665
|
+
console.log(" Memory .......... yes");
|
|
666
|
+
if (hasMissions) {
|
|
667
|
+
const n = fs.readdirSync(path.join(polpoDir, "missions")).filter(f => f.endsWith(".json")).length;
|
|
668
|
+
console.log(` Missions ........ ${n}`);
|
|
669
|
+
}
|
|
670
|
+
if (hasPlaybooks)
|
|
671
|
+
console.log(" Playbooks ....... yes");
|
|
672
|
+
if (hasSkills) {
|
|
673
|
+
const n = fs.readdirSync(path.join(polpoDir, "skills")).filter((d) => fs.statSync(path.join(polpoDir, "skills", d)).isDirectory()).length;
|
|
674
|
+
console.log(` Skills .......... ${n}`);
|
|
675
|
+
}
|
|
676
|
+
if (hasSchedules) {
|
|
677
|
+
const n = fs.readdirSync(path.join(polpoDir, "schedules")).filter(f => f.endsWith(".json")).length;
|
|
678
|
+
console.log(` Schedules ....... ${n}`);
|
|
679
|
+
}
|
|
680
|
+
if (hasVault)
|
|
681
|
+
console.log(" Vault ........... yes");
|
|
682
|
+
if (hasAvatars)
|
|
683
|
+
console.log(" Avatars ......... yes");
|
|
684
|
+
if (includeTasks && hasTasks)
|
|
685
|
+
console.log(" Tasks ........... yes");
|
|
686
|
+
if (includeSessions && hasSessions)
|
|
687
|
+
console.log(" Sessions ........ yes");
|
|
688
|
+
console.log("");
|
|
689
|
+
if (!force && isTTY()) {
|
|
690
|
+
const ok = await confirm(" Deploy?");
|
|
691
|
+
if (!ok) {
|
|
692
|
+
console.log(" Aborted.");
|
|
693
|
+
process.exit(0);
|
|
694
|
+
}
|
|
695
|
+
console.log();
|
|
696
|
+
}
|
|
697
|
+
// ── Step 4: Deploy ────────────────────────
|
|
698
|
+
console.log(" Deploying...");
|
|
699
|
+
const total = emptyResult();
|
|
700
|
+
if (hasTeams) {
|
|
701
|
+
mergeResult(total, await deployTeams(client, polpoDir));
|
|
702
|
+
}
|
|
703
|
+
if (hasAgents) {
|
|
704
|
+
mergeResult(total, await deployAgents(client, polpoDir, force));
|
|
705
|
+
}
|
|
706
|
+
if (hasMemory) {
|
|
707
|
+
mergeResult(total, await deployMemory(client, polpoDir));
|
|
708
|
+
}
|
|
709
|
+
if (hasMissions) {
|
|
710
|
+
mergeResult(total, await deployMissions(client, polpoDir));
|
|
711
|
+
}
|
|
712
|
+
if (hasPlaybooks) {
|
|
713
|
+
mergeResult(total, await deployPlaybooks(client, polpoDir));
|
|
714
|
+
}
|
|
715
|
+
if (hasSkills) {
|
|
716
|
+
mergeResult(total, await deploySkills(client, polpoDir, force));
|
|
717
|
+
}
|
|
718
|
+
if (hasSchedules) {
|
|
719
|
+
mergeResult(total, await deploySchedules(client, polpoDir));
|
|
720
|
+
}
|
|
721
|
+
if (hasVault) {
|
|
722
|
+
mergeResult(total, await deployVault(client, polpoDir));
|
|
723
|
+
}
|
|
724
|
+
if (hasAvatars) {
|
|
725
|
+
mergeResult(total, await deployAvatars(client, polpoDir, creds.baseUrl, creds.apiKey));
|
|
726
|
+
}
|
|
727
|
+
if (includeTasks && hasTasks) {
|
|
728
|
+
mergeResult(total, await deployTasks(client, polpoDir));
|
|
729
|
+
}
|
|
730
|
+
if (includeSessions && hasSessions) {
|
|
731
|
+
mergeResult(total, await deploySessions(client, polpoDir));
|
|
732
|
+
}
|
|
733
|
+
// ── Summary ────────────────────────
|
|
734
|
+
const parts = [];
|
|
735
|
+
if (total.created > 0)
|
|
736
|
+
parts.push(`${total.created} created`);
|
|
737
|
+
if (total.updated > 0)
|
|
738
|
+
parts.push(`${total.updated} updated`);
|
|
739
|
+
if (total.skipped > 0)
|
|
740
|
+
parts.push(`${total.skipped} skipped`);
|
|
741
|
+
if (total.failed > 0)
|
|
742
|
+
parts.push(`${total.failed} failed`);
|
|
743
|
+
if (total.errors.length > 0) {
|
|
744
|
+
console.log("\n Errors:");
|
|
745
|
+
for (const err of total.errors)
|
|
746
|
+
console.log(` - ${err}`);
|
|
747
|
+
}
|
|
748
|
+
console.log(`\n Result: ${parts.join(", ") || "nothing to deploy"}\n`);
|
|
749
|
+
process.exit(total.failed > 0 ? 1 : 0);
|
|
750
|
+
});
|
|
751
|
+
}
|
|
752
|
+
//# sourceMappingURL=deploy.js.map
|