@polpo-ai/cli 0.6.4 → 0.6.6

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.
@@ -18,14 +18,16 @@
18
18
  import * as fs from "node:fs";
19
19
  import * as path from "node:path";
20
20
  import pc from "picocolors";
21
+ import * as clack from "@clack/prompts";
21
22
  import { createApiClient } from "./api.js";
22
- import { isTTY, confirm } from "./prompt.js";
23
23
  import { resolveKey, decrypt } from "@polpo-ai/vault-crypto";
24
24
  import { AddAgentSchema } from "@polpo-ai/server";
25
25
  import { friendlyError } from "../../util/errors.js";
26
26
  import { pickOrg } from "../../util/org.js";
27
27
  import { resolveOrCreateProject } from "../../util/project.js";
28
28
  import { requireAuth } from "../../util/auth.js";
29
+ import { isTTY } from "./prompt.js";
30
+ import { resolveDeployConflict } from "../../util/conflicts.js";
29
31
  function emptyResult() {
30
32
  return { created: 0, updated: 0, skipped: 0, failed: 0, errors: [] };
31
33
  }
@@ -70,33 +72,64 @@ function listJsonFiles(dir) {
70
72
  return fs.readdirSync(dir).filter(f => f.endsWith(".json")).map(f => path.join(dir, f));
71
73
  }
72
74
  // ── Core deployers ──────────────────────────────────────
73
- async function deployTeams(client, polpoDir) {
75
+ async function deployTeams(client, polpoDir, opts) {
74
76
  const result = emptyResult();
75
77
  const teams = loadJson(path.join(polpoDir, "teams.json"));
76
78
  if (!teams || !Array.isArray(teams))
77
79
  return result;
80
+ // Fetch existing teams for conflict detection
81
+ let existingTeams = {};
82
+ try {
83
+ const res = await client.get("/v1/agents/teams");
84
+ if (res.status === 200) {
85
+ const data = res.data?.data ?? res.data ?? [];
86
+ if (Array.isArray(data)) {
87
+ for (const t of data)
88
+ existingTeams[t.name] = t;
89
+ }
90
+ }
91
+ }
92
+ catch { /* proceed without comparison */ }
78
93
  for (const team of teams) {
79
94
  if (!team.name || typeof team.name !== "string") {
80
95
  result.errors.push(`team missing "name" field`);
81
96
  result.failed++;
82
97
  continue;
83
98
  }
84
- const res = await client.post("/v1/agents/teams", { name: team.name, description: team.description });
85
- if (res.status >= 200 && res.status < 300) {
86
- result.created++;
87
- }
88
- else if (res.status === 409 || res.data?.error?.includes("already exists")) {
99
+ const remote = existingTeams[team.name];
100
+ const local = { name: team.name, description: team.description };
101
+ const action = await resolveDeployConflict(local, remote, `team "${team.name}"`, opts);
102
+ if (action === "skip") {
89
103
  result.skipped++;
104
+ continue;
105
+ }
106
+ if (remote) {
107
+ // Update existing
108
+ const res = await client.put(`/v1/agents/team`, { name: team.name, description: team.description });
109
+ if (res.status >= 200 && res.status < 300) {
110
+ result.updated++;
111
+ }
112
+ else {
113
+ const msg = res.data?.error ?? `HTTP ${res.status}`;
114
+ result.errors.push(`team "${team.name}": ${friendlyError(msg)}`);
115
+ result.failed++;
116
+ }
90
117
  }
91
118
  else {
92
- const msg = res.data?.error ?? `HTTP ${res.status}`;
93
- result.errors.push(`team "${team.name}": ${friendlyError(msg)}`);
94
- result.failed++;
119
+ const res = await client.post("/v1/agents/teams", local);
120
+ if (res.status >= 200 && res.status < 300) {
121
+ result.created++;
122
+ }
123
+ else {
124
+ const msg = res.data?.error ?? `HTTP ${res.status}`;
125
+ result.errors.push(`team "${team.name}": ${friendlyError(msg)}`);
126
+ result.failed++;
127
+ }
95
128
  }
96
129
  }
97
130
  return result;
98
131
  }
99
- async function deployAgents(client, polpoDir, force) {
132
+ async function deployAgents(client, polpoDir, opts) {
100
133
  const result = emptyResult();
101
134
  const raw = loadJson(path.join(polpoDir, "agents.json"));
102
135
  if (!raw || !Array.isArray(raw)) {
@@ -106,21 +139,22 @@ async function deployAgents(client, polpoDir, force) {
106
139
  }
107
140
  return result;
108
141
  }
109
- // Fetch existing agents for upsert detection
110
- let existingNames = new Set();
142
+ // Fetch existing agents for conflict detection
143
+ let existingAgents = {};
111
144
  try {
112
145
  const res = await client.get("/v1/agents");
113
146
  if (res.status === 200) {
114
147
  const data = res.data?.data ?? res.data ?? [];
115
- if (Array.isArray(data))
116
- existingNames = new Set(data.map((a) => a.name));
148
+ if (Array.isArray(data)) {
149
+ for (const a of data)
150
+ existingAgents[a.name] = a;
151
+ }
117
152
  }
118
153
  }
119
- catch { /* can't check will try create */ }
154
+ catch { /* proceed without comparison */ }
120
155
  for (const entry of raw) {
121
156
  const agent = entry.agent ?? entry;
122
157
  const teamName = entry.teamName ?? "default";
123
- // Validate agent schema
124
158
  const parsed = AddAgentSchema.safeParse(agent);
125
159
  if (!parsed.success) {
126
160
  const issues = parsed.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join(", ");
@@ -128,15 +162,13 @@ async function deployAgents(client, polpoDir, force) {
128
162
  result.failed++;
129
163
  continue;
130
164
  }
131
- const exists = existingNames.has(agent.name);
132
- if (exists) {
133
- if (!force && isTTY()) {
134
- const ok = await confirm(` Agent "${agent.name}" already exists. Override?`);
135
- if (!ok) {
136
- result.skipped++;
137
- continue;
138
- }
139
- }
165
+ const remote = existingAgents[agent.name];
166
+ const action = await resolveDeployConflict(agent, remote, `agent "${agent.name}"`, opts);
167
+ if (action === "skip") {
168
+ result.skipped++;
169
+ continue;
170
+ }
171
+ if (remote) {
140
172
  const res = await client.put(`/v1/agents/${encodeURIComponent(agent.name)}`, { ...agent, team: teamName });
141
173
  if (res.status >= 200 && res.status < 300) {
142
174
  result.updated++;
@@ -161,17 +193,31 @@ async function deployAgents(client, polpoDir, force) {
161
193
  }
162
194
  return result;
163
195
  }
164
- async function deployMemory(client, polpoDir) {
196
+ async function deployMemory(client, polpoDir, opts) {
165
197
  const result = emptyResult();
166
198
  const shared = loadText(path.join(polpoDir, "memory.md"));
167
199
  if (shared) {
168
- const res = await client.put("/v1/memory", { content: shared });
169
- if (res.status >= 200 && res.status < 300) {
170
- result.updated++;
200
+ // Fetch existing shared memory for comparison
201
+ let remoteShared = null;
202
+ try {
203
+ const r = await client.get("/v1/memory");
204
+ if (r.status === 200)
205
+ remoteShared = r.data?.content ?? null;
206
+ }
207
+ catch { }
208
+ const action = await resolveDeployConflict(shared, remoteShared, "shared memory", opts);
209
+ if (action === "write") {
210
+ const res = await client.put("/v1/memory", { content: shared });
211
+ if (res.status >= 200 && res.status < 300) {
212
+ result.updated++;
213
+ }
214
+ else {
215
+ result.errors.push(`memory: ${friendlyError(res.data?.error ?? `HTTP ${res.status}`)}`);
216
+ result.failed++;
217
+ }
171
218
  }
172
219
  else {
173
- result.errors.push(`memory: ${friendlyError(res.data?.error ?? `HTTP ${res.status}`)}`);
174
- result.failed++;
220
+ result.skipped++;
175
221
  }
176
222
  }
177
223
  const memDir = path.join(polpoDir, "memory");
@@ -180,13 +226,26 @@ async function deployMemory(client, polpoDir) {
180
226
  const agentName = file.replace(".md", "");
181
227
  const content = loadText(path.join(memDir, file));
182
228
  if (content) {
183
- const res = await client.put(`/v1/memory/agent/${agentName}`, { content });
184
- if (res.status >= 200 && res.status < 300) {
185
- result.updated++;
229
+ let remoteAgent = null;
230
+ try {
231
+ const r = await client.get(`/v1/memory/agent/${agentName}`);
232
+ if (r.status === 200)
233
+ remoteAgent = r.data?.content ?? null;
234
+ }
235
+ catch { }
236
+ const action = await resolveDeployConflict(content, remoteAgent, `memory "${agentName}"`, opts);
237
+ if (action === "write") {
238
+ const res = await client.put(`/v1/memory/agent/${agentName}`, { content });
239
+ if (res.status >= 200 && res.status < 300) {
240
+ result.updated++;
241
+ }
242
+ else {
243
+ result.errors.push(`memory "${agentName}": ${friendlyError(res.data?.error ?? `HTTP ${res.status}`)}`);
244
+ result.failed++;
245
+ }
186
246
  }
187
247
  else {
188
- result.errors.push(`memory "${agentName}": ${friendlyError(res.data?.error ?? `HTTP ${res.status}`)}`);
189
- result.failed++;
248
+ result.skipped++;
190
249
  }
191
250
  }
192
251
  }
@@ -249,11 +308,24 @@ async function deployPlaybooks(client, polpoDir) {
249
308
  }
250
309
  return result;
251
310
  }
252
- async function deploySkills(client, polpoDir, force) {
311
+ async function deploySkills(client, polpoDir, opts) {
253
312
  const result = emptyResult();
254
313
  const skillsDir = path.join(polpoDir, "skills");
255
314
  if (!fs.existsSync(skillsDir))
256
315
  return result;
316
+ // Fetch existing skills for conflict detection
317
+ let existingSkills = {};
318
+ try {
319
+ const res = await client.get("/v1/skills");
320
+ if (res.status === 200) {
321
+ const data = res.data?.data ?? res.data ?? [];
322
+ if (Array.isArray(data)) {
323
+ for (const s of data)
324
+ existingSkills[s.name] = s;
325
+ }
326
+ }
327
+ }
328
+ catch { }
257
329
  for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
258
330
  if (!entry.isDirectory())
259
331
  continue;
@@ -292,36 +364,40 @@ async function deploySkills(client, polpoDir, force) {
292
364
  }
293
365
  const bodyMatch = raw.match(/^---\n[\s\S]*?\n---\n?([\s\S]*)$/);
294
366
  const content = bodyMatch ? bodyMatch[1].trim() : raw.trim();
295
- // Try create first
296
- const res = await client.post("/v1/skills/create", {
297
- name, description, content,
298
- ...(allowedTools?.length ? { allowedTools } : {}),
299
- });
300
- if (res.status >= 200 && res.status < 300) {
301
- result.created++;
367
+ const localSkill = { name, description, content };
368
+ const remote = existingSkills[name];
369
+ const action = await resolveDeployConflict(localSkill, remote ? { name: remote.name, description: remote.description, content: remote.content } : null, `skill "${name}"`, opts);
370
+ if (action === "skip") {
371
+ result.skipped++;
372
+ continue;
302
373
  }
303
- else if (res.status === 409 || res.data?.error?.includes("already exists")) {
304
- if (force) {
305
- // Update existing skill
306
- const updateRes = await client.put(`/v1/skills/${encodeURIComponent(name)}`, {
307
- description, content,
308
- ...(allowedTools?.length ? { allowedTools } : {}),
309
- });
310
- if (updateRes.status >= 200 && updateRes.status < 300) {
311
- result.updated++;
312
- }
313
- else {
314
- result.skipped++;
315
- }
374
+ if (remote) {
375
+ const updateRes = await client.put(`/v1/skills/${encodeURIComponent(name)}`, {
376
+ description, content,
377
+ ...(allowedTools?.length ? { allowedTools } : {}),
378
+ });
379
+ if (updateRes.status >= 200 && updateRes.status < 300) {
380
+ result.updated++;
316
381
  }
317
382
  else {
318
- result.skipped++;
383
+ const msg = updateRes.data?.error ?? `HTTP ${updateRes.status}`;
384
+ result.errors.push(`skill "${name}": ${friendlyError(msg)}`);
385
+ result.failed++;
319
386
  }
320
387
  }
321
388
  else {
322
- const msg = res.data?.error ?? `HTTP ${res.status}`;
323
- result.errors.push(`skill "${name}": ${friendlyError(msg)}`);
324
- result.failed++;
389
+ const res = await client.post("/v1/skills/create", {
390
+ name, description, content,
391
+ ...(allowedTools?.length ? { allowedTools } : {}),
392
+ });
393
+ if (res.status >= 200 && res.status < 300) {
394
+ result.created++;
395
+ }
396
+ else {
397
+ const msg = res.data?.error ?? `HTTP ${res.status}`;
398
+ result.errors.push(`skill "${name}": ${friendlyError(msg)}`);
399
+ result.failed++;
400
+ }
325
401
  }
326
402
  }
327
403
  return result;
@@ -513,9 +589,7 @@ export function registerDeployCommand(program) {
513
589
  .option("--include-sessions", "Also deploy chat sessions")
514
590
  .option("--all", "Deploy everything (full local→cloud migration)")
515
591
  .action(async (opts) => {
516
- // requireAuth auto-triggers device-code login if creds are missing/expired,
517
- // so a fresh user typing `polpo deploy` first goes through browser auth
518
- // instead of getting a "Not logged in" wall.
592
+ clack.intro(pc.bold("Polpo Deploy"));
519
593
  const creds = await requireAuth({
520
594
  context: "Deploying requires an authenticated session.",
521
595
  });
@@ -523,17 +597,14 @@ export function registerDeployCommand(program) {
523
597
  const polpoConfig = loadJson(path.join(polpoDir, "polpo.json"));
524
598
  const projectName = polpoConfig?.project ?? path.basename(path.resolve(opts.dir));
525
599
  const force = opts.force || opts.yes || false;
526
- // Control plane client (no project context needed for orgs/projects)
600
+ const interactive = !force && isTTY();
527
601
  const cpClient = createApiClient(creds);
528
- console.log("\n Polpo Deploy\n");
602
+ const s = clack.spinner();
529
603
  // ── Step 1: Resolve project ────────────────────────
530
604
  let projectId = polpoConfig?.projectId;
531
605
  let projectSlug = polpoConfig?.projectSlug;
532
606
  if (!projectId) {
533
607
  try {
534
- // pickOrg handles the 0-org case inline (prompts to create one),
535
- // so a fresh user running `polpo deploy` against an empty account
536
- // gets a single graceful prompt instead of an exit.
537
608
  const org = await pickOrg(cpClient);
538
609
  const project = await resolveOrCreateProject({
539
610
  client: cpClient,
@@ -544,16 +615,16 @@ export function registerDeployCommand(program) {
544
615
  });
545
616
  projectId = project.id;
546
617
  projectSlug = project.slug;
547
- console.log(` Project: ${project.name}\n`);
618
+ clack.log.success(`Project: ${pc.bold(project.name)}`);
548
619
  }
549
620
  catch (err) {
550
621
  const msg = err instanceof Error ? err.message : String(err);
551
- console.error(` ${friendlyError(msg)}`);
622
+ clack.outro(pc.red(friendlyError(msg)));
552
623
  process.exit(1);
553
624
  }
554
625
  }
555
626
  if (!projectId) {
556
- console.error(" No project resolved. Deploy from a project directory with .polpo/polpo.json");
627
+ clack.outro(pc.red("No project resolved. Deploy from a directory with .polpo/polpo.json"));
557
628
  process.exit(1);
558
629
  }
559
630
  // Backfill `projectSlug` for users with legacy polpo.json (id only).
@@ -610,28 +681,27 @@ export function registerDeployCommand(program) {
610
681
  }
611
682
  }
612
683
  if (detected.length > 0) {
613
- console.log(" Detected LLM keys:");
614
- for (const { envVar, value } of detected) {
615
- console.log(` ${envVar.padEnd(25)} ${value.slice(0, 8)}...${value.slice(-4)}`);
616
- }
617
- console.log();
618
- if (isTTY() && !force) {
619
- const push = await confirm(" Push LLM keys to cloud?");
620
- if (push) {
621
- let n = 0;
622
- for (const { provider, value } of detected) {
623
- try {
624
- await cpClient.post("/v1/byok", { provider, key: value });
625
- n++;
626
- }
627
- catch { }
684
+ clack.log.info(`Detected LLM keys:\n` +
685
+ detected.map(({ envVar, value }) => ` ${pc.dim(envVar.padEnd(25))} ${pc.bold(value.slice(0, 8))}...${value.slice(-4)}`).join("\n"));
686
+ let pushKeys = force;
687
+ if (!pushKeys && interactive) {
688
+ const answer = await clack.confirm({
689
+ message: "Push LLM keys to cloud?",
690
+ initialValue: true,
691
+ });
692
+ pushKeys = !clack.isCancel(answer) && !!answer;
693
+ }
694
+ if (pushKeys) {
695
+ s.start("Pushing LLM keys...");
696
+ let n = 0;
697
+ for (const { provider, value } of detected) {
698
+ try {
699
+ await cpClient.post("/v1/byok", { provider, key: value });
700
+ n++;
628
701
  }
629
- if (n > 0)
630
- console.log(` Pushed ${n} LLM key(s)\n`);
631
- }
632
- else {
633
- console.log();
702
+ catch { }
634
703
  }
704
+ s.stop(n > 0 ? `Pushed ${n} LLM key(s)` : "No keys pushed");
635
705
  }
636
706
  }
637
707
  // ── Step 3: Scan & show resources ────────────────────
@@ -655,105 +725,155 @@ export function registerDeployCommand(program) {
655
725
  fs.readdirSync(path.join(polpoDir, "sessions")).length > 0;
656
726
  const includeTasks = opts.all || opts.includeTasks;
657
727
  const includeSessions = opts.all || opts.includeSessions;
658
- console.log(" Resources to deploy:");
728
+ // Build resource summary lines
729
+ const resourceLines = [];
659
730
  if (hasAgents) {
660
731
  const agentsData = loadJson(path.join(polpoDir, "agents.json"));
661
732
  if (Array.isArray(agentsData)) {
662
733
  const names = agentsData.map((e) => (e.agent ?? e).name).filter(Boolean);
663
- console.log(` Agents .......... ${names.length} (${names.join(", ")})`);
734
+ resourceLines.push(` ${pc.bold("Agents")} ${names.length} ${pc.dim(`(${names.join(", ")})`)}`);
664
735
  }
665
736
  }
666
737
  if (hasTeams) {
667
738
  const teamsData = loadJson(path.join(polpoDir, "teams.json"));
668
739
  if (Array.isArray(teamsData)) {
669
- console.log(` Teams ........... ${teamsData.length} (${teamsData.map((t) => t.name).join(", ")})`);
740
+ resourceLines.push(` ${pc.bold("Teams")} ${teamsData.length} ${pc.dim(`(${teamsData.map((t) => t.name).join(", ")})`)}`);
670
741
  }
671
742
  }
672
743
  if (hasMemory)
673
- console.log(" Memory .......... yes");
744
+ resourceLines.push(` ${pc.bold("Memory")} ${pc.dim("shared + agent")}`);
674
745
  if (hasMissions) {
675
746
  const n = fs.readdirSync(path.join(polpoDir, "missions")).filter(f => f.endsWith(".json")).length;
676
- console.log(` Missions ........ ${n}`);
747
+ resourceLines.push(` ${pc.bold("Missions")} ${n}`);
677
748
  }
678
749
  if (hasPlaybooks)
679
- console.log(" Playbooks ....... yes");
750
+ resourceLines.push(` ${pc.bold("Playbooks")} yes`);
680
751
  if (hasSkills) {
681
752
  const n = fs.readdirSync(path.join(polpoDir, "skills")).filter((d) => fs.statSync(path.join(polpoDir, "skills", d)).isDirectory()).length;
682
- console.log(` Skills .......... ${n}`);
753
+ resourceLines.push(` ${pc.bold("Skills")} ${n}`);
683
754
  }
684
755
  if (hasSchedules) {
685
756
  const n = fs.readdirSync(path.join(polpoDir, "schedules")).filter(f => f.endsWith(".json")).length;
686
- console.log(` Schedules ....... ${n}`);
757
+ resourceLines.push(` ${pc.bold("Schedules")} ${n}`);
687
758
  }
688
759
  if (hasVault)
689
- console.log(" Vault ........... yes");
760
+ resourceLines.push(` ${pc.bold("Vault")} ${pc.dim("encrypted credentials")}`);
690
761
  if (hasAvatars)
691
- console.log(" Avatars ......... yes");
762
+ resourceLines.push(` ${pc.bold("Avatars")} yes`);
692
763
  if (includeTasks && hasTasks)
693
- console.log(" Tasks ........... yes");
764
+ resourceLines.push(` ${pc.bold("Tasks")} yes`);
694
765
  if (includeSessions && hasSessions)
695
- console.log(" Sessions ........ yes");
696
- console.log("");
697
- if (!force && isTTY()) {
698
- const ok = await confirm(" Deploy?");
699
- if (!ok) {
700
- console.log(" Aborted.");
766
+ resourceLines.push(` ${pc.bold("Sessions")} yes`);
767
+ if (resourceLines.length === 0) {
768
+ clack.outro(pc.yellow("Nothing to deploy — .polpo/ has no resources."));
769
+ process.exit(0);
770
+ }
771
+ clack.log.info(`Resources to deploy:\n${resourceLines.join("\n")}`);
772
+ if (interactive) {
773
+ const ok = await clack.confirm({
774
+ message: "Deploy these resources to cloud?",
775
+ initialValue: true,
776
+ });
777
+ if (clack.isCancel(ok) || !ok) {
778
+ clack.outro(pc.dim("Deploy cancelled."));
701
779
  process.exit(0);
702
780
  }
703
- console.log();
704
781
  }
705
- // ── Step 4: Deploy ────────────────────────
706
- console.log(" Deploying...");
782
+ // ── Step 4: Deploy each resource ────────────────────
707
783
  const total = emptyResult();
784
+ const conflictOpts = { force, interactive };
708
785
  if (hasTeams) {
709
- mergeResult(total, await deployTeams(client, polpoDir));
786
+ s.start("Deploying teams...");
787
+ const r = await deployTeams(client, polpoDir, conflictOpts);
788
+ mergeResult(total, r);
789
+ s.stop(`Teams: ${r.created} created, ${r.updated} updated${r.skipped ? `, ${r.skipped} skipped` : ""}${r.failed ? `, ${r.failed} failed` : ""}`);
710
790
  }
711
791
  if (hasAgents) {
712
- mergeResult(total, await deployAgents(client, polpoDir, force));
792
+ s.start("Deploying agents...");
793
+ const r = await deployAgents(client, polpoDir, conflictOpts);
794
+ mergeResult(total, r);
795
+ s.stop(`Agents: ${r.created} created, ${r.updated} updated${r.skipped ? `, ${r.skipped} skipped` : ""}${r.failed ? `, ${r.failed} failed` : ""}`);
713
796
  }
714
797
  if (hasMemory) {
715
- mergeResult(total, await deployMemory(client, polpoDir));
798
+ s.start("Deploying memory...");
799
+ const r = await deployMemory(client, polpoDir, conflictOpts);
800
+ mergeResult(total, r);
801
+ s.stop(`Memory: ${r.updated} updated${r.skipped ? `, ${r.skipped} skipped` : ""}${r.failed ? `, ${r.failed} failed` : ""}`);
716
802
  }
717
803
  if (hasMissions) {
718
- mergeResult(total, await deployMissions(client, polpoDir));
804
+ s.start("Deploying missions...");
805
+ const r = await deployMissions(client, polpoDir);
806
+ mergeResult(total, r);
807
+ s.stop(`Missions: ${r.created} created${r.failed ? `, ${r.failed} failed` : ""}`);
719
808
  }
720
809
  if (hasPlaybooks) {
721
- mergeResult(total, await deployPlaybooks(client, polpoDir));
810
+ s.start("Deploying playbooks...");
811
+ const r = await deployPlaybooks(client, polpoDir);
812
+ mergeResult(total, r);
813
+ s.stop(`Playbooks: ${r.created} created${r.failed ? `, ${r.failed} failed` : ""}`);
722
814
  }
723
815
  if (hasSkills) {
724
- mergeResult(total, await deploySkills(client, polpoDir, force));
816
+ s.start("Deploying skills...");
817
+ const r = await deploySkills(client, polpoDir, conflictOpts);
818
+ mergeResult(total, r);
819
+ s.stop(`Skills: ${r.created} created, ${r.updated} updated${r.skipped ? `, ${r.skipped} skipped` : ""}${r.failed ? `, ${r.failed} failed` : ""}`);
725
820
  }
726
821
  if (hasSchedules) {
727
- mergeResult(total, await deploySchedules(client, polpoDir));
822
+ s.start("Deploying schedules...");
823
+ const r = await deploySchedules(client, polpoDir);
824
+ mergeResult(total, r);
825
+ s.stop(`Schedules: ${r.created} created${r.failed ? `, ${r.failed} failed` : ""}`);
728
826
  }
729
827
  if (hasVault) {
730
- mergeResult(total, await deployVault(client, polpoDir));
828
+ s.start("Deploying vault...");
829
+ const r = await deployVault(client, polpoDir);
830
+ mergeResult(total, r);
831
+ s.stop(`Vault: ${r.created} created${r.failed ? `, ${r.failed} failed` : ""}`);
731
832
  }
732
833
  if (hasAvatars) {
733
- mergeResult(total, await deployAvatars(client, polpoDir, creds.baseUrl, creds.apiKey));
834
+ s.start("Deploying avatars...");
835
+ const r = await deployAvatars(client, polpoDir, creds.baseUrl, creds.apiKey);
836
+ mergeResult(total, r);
837
+ s.stop(`Avatars: ${r.created} uploaded${r.failed ? `, ${r.failed} failed` : ""}`);
734
838
  }
735
839
  if (includeTasks && hasTasks) {
736
- mergeResult(total, await deployTasks(client, polpoDir));
840
+ s.start("Deploying tasks...");
841
+ const r = await deployTasks(client, polpoDir);
842
+ mergeResult(total, r);
843
+ s.stop(`Tasks: ${r.created} created${r.failed ? `, ${r.failed} failed` : ""}`);
737
844
  }
738
845
  if (includeSessions && hasSessions) {
739
- mergeResult(total, await deploySessions(client, polpoDir));
846
+ s.start("Deploying sessions...");
847
+ const r = await deploySessions(client, polpoDir);
848
+ mergeResult(total, r);
849
+ s.stop(`Sessions: ${r.created} imported${r.failed ? `, ${r.failed} failed` : ""}`);
740
850
  }
741
851
  // ── Summary ────────────────────────
742
- const parts = [];
852
+ if (total.errors.length > 0) {
853
+ clack.log.warn(`Errors:\n` +
854
+ total.errors.map(e => ` ${pc.red("x")} ${e}`).join("\n"));
855
+ }
856
+ const summaryParts = [];
743
857
  if (total.created > 0)
744
- parts.push(`${total.created} created`);
858
+ summaryParts.push(`${total.created} created`);
745
859
  if (total.updated > 0)
746
- parts.push(`${total.updated} updated`);
860
+ summaryParts.push(`${total.updated} updated`);
747
861
  if (total.skipped > 0)
748
- parts.push(`${total.skipped} skipped`);
862
+ summaryParts.push(`${total.skipped} skipped`);
749
863
  if (total.failed > 0)
750
- parts.push(`${total.failed} failed`);
751
- if (total.errors.length > 0) {
752
- console.log("\n Errors:");
753
- for (const err of total.errors)
754
- console.log(` - ${err}`);
864
+ summaryParts.push(pc.red(`${total.failed} failed`));
865
+ const endpoint = projectSlug ? `https://${projectSlug}.polpo.cloud` : "";
866
+ const outroLines = [];
867
+ if (total.failed === 0) {
868
+ outroLines.push(pc.green(`✓ Deployed: ${summaryParts.join(", ")}`));
869
+ }
870
+ else {
871
+ outroLines.push(pc.yellow(`Deployed with errors: ${summaryParts.join(", ")}`));
872
+ }
873
+ if (endpoint) {
874
+ outroLines.push(pc.dim(` Endpoint: ${pc.bold(endpoint)}`));
755
875
  }
756
- console.log(`\n Result: ${parts.join(", ") || "nothing to deploy"}\n`);
876
+ clack.outro(outroLines.join("\n"));
757
877
  process.exit(total.failed > 0 ? 1 : 0);
758
878
  });
759
879
  }